diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index fff2f01..d6bb7b0 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -43,6 +43,41 @@ def me(req: Request, db: Session = Depends(get_db)): return {"id": user.id, "email": user.email, "role": user.role, "display_name": user.display_name} +@router.get("/me/stats") +def my_stats(req: Request, db: Session = Depends(get_db)): + uid = get_session_user_id(req) + if not uid: + raise HTTPException(status_code=401, detail="not logged in") + + # "played" = games where user is member AND winner is set (finished games) + from sqlalchemy import func + from ..models import Game, GameMember + + played = ( + db.query(func.count(Game.id)) + .join(GameMember, GameMember.game_id == Game.id) + .filter(GameMember.user_id == uid, Game.winner_user_id != None) + .scalar() + or 0 + ) + + wins = ( + db.query(func.count(Game.id)) + .join(GameMember, GameMember.game_id == Game.id) + .filter(GameMember.user_id == uid, Game.winner_user_id == uid) + .scalar() + or 0 + ) + + losses = max(int(played) - int(wins), 0) + winrate = (float(wins) / float(played) * 100.0) if played else 0.0 + + return { + "played": int(played), + "wins": int(wins), + "losses": int(losses), + "winrate": round(winrate, 1), + } @router.patch("/password") def set_password(data: dict, req: Request, db: Session = Depends(get_db)): diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bb7cc5f..d74593c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,6 +20,7 @@ import DesignModal from "./components/DesignModal"; import WinnerCard from "./components/WinnerCard"; import WinnerBadge from "./components/WinnerBadge"; import NewGameModal from "./components/NewGameModal"; +import StatsModal from "./components/StatsModal"; export default function App() { useHpGlobalStyles(); @@ -62,6 +63,12 @@ export default function App() { // New Game Modal const [newGameOpen, setNewGameOpen] = useState(false); + // ===== Stats Modal ===== + const [statsOpen, setStatsOpen] = useState(false); + const [stats, setStats] = useState(null); + const [statsLoading, setStatsLoading] = useState(false); + const [statsError, setStatsError] = useState(""); + const load = async () => { const m = await api("/auth/me"); setMe(m); @@ -201,6 +208,29 @@ export default function App() { } }; + // ===== Stats (always fresh on open) ===== + const openStatsModal = async () => { + setUserMenuOpen(false); + setStatsOpen(true); + setStatsError(""); + setStatsLoading(true); + + try { + const s = await api("/auth/me/stats"); + setStats(s); + } catch (e) { + setStats(null); + setStatsError("❌ Fehler: " + (e?.message || "unknown")); + } finally { + setStatsLoading(false); + } + }; + + const closeStatsModal = () => { + setStatsOpen(false); + setStatsError(""); + }; + // ===== New game flow ===== const createGame = async () => { const g = await api("/games", { @@ -366,6 +396,7 @@ export default function App() { setUserMenuOpen={setUserMenuOpen} openPwModal={openPwModal} openDesignModal={openDesignModal} + openStatsModal={openStatsModal} doLogout={doLogout} onOpenNewGame={() => setNewGameOpen(true)} /> @@ -444,6 +475,15 @@ export default function App() { closeChipModalToDash={closeChipModalToDash} chooseChip={chooseChip} /> + + ); } diff --git a/frontend/src/components/StatsModal.jsx b/frontend/src/components/StatsModal.jsx new file mode 100644 index 0000000..db6b91d --- /dev/null +++ b/frontend/src/components/StatsModal.jsx @@ -0,0 +1,101 @@ +import React from "react"; +import { createPortal } from "react-dom"; +import { styles } from "../styles/styles"; +import { stylesTokens } from "../styles/theme"; + +function Tile({ label, value, sub }) { + return ( +
+
+ {label} +
+ +
+ {value} +
+ + {sub ? ( +
+ {sub} +
+ ) : null} +
+ ); +} + +export default function StatsModal({ open, onClose, me, stats, loading, error }) { + if (!open) return null; + + const displayName = me ? ((me.display_name || "").trim() || me.email) : ""; + + return createPortal( +
+
e.stopPropagation()}> +
+
+
Statistik
+
+ {displayName} +
+
+ + +
+ +
+ {loading ? ( +
+ Lade Statistik… +
+ ) : error ? ( +
{error}
+ ) : ( +
+ + + + +
+ )} +
+ +
+ Hinweis: „Gespielt“ zählt nur Spiele mit gesetztem Sieger. +
+
+
, + document.body + ); +} diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index 5cb2527..ab4b407 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -8,19 +8,16 @@ export default function TopBar({ setUserMenuOpen, openPwModal, openDesignModal, + openStatsModal, doLogout, onOpenNewGame, }) { - const displayName = me - ? ((me.display_name || "").trim() || me.email) - : ""; + const displayName = me ? ((me.display_name || "").trim() || me.email) : ""; return (
-
- Notizbogen -
+
Notizbogen
{displayName}
@@ -52,6 +49,18 @@ export default function TopBar({ {me?.email || ""}
+ + +
+ diff --git a/frontend/src/components/WinnerBadge.jsx b/frontend/src/components/WinnerBadge.jsx index 0ed5fe9..ae66f41 100644 --- a/frontend/src/components/WinnerBadge.jsx +++ b/frontend/src/components/WinnerBadge.jsx @@ -49,7 +49,7 @@ export default function WinnerBadge({ winner, winnerEmail }) { {showEmail && (
- {winner.displayName} + {winner.display_name}
)}
diff --git a/frontend/src/components/WinnerCard.jsx b/frontend/src/components/WinnerCard.jsx index c16a12b..ad7d625 100644 --- a/frontend/src/components/WinnerCard.jsx +++ b/frontend/src/components/WinnerCard.jsx @@ -25,7 +25,7 @@ export default function WinnerCard({ {members.map((m) => ( ))}