diff --git a/src/components/GameTable.tsx b/src/components/GameTable.tsx
index f8b6526..6d8c5c4 100644
--- a/src/components/GameTable.tsx
+++ b/src/components/GameTable.tsx
@@ -62,6 +62,9 @@ export function GameTable({
const muted = !sfx && !music;
const exit = onExit ?? reset;
+ const vw = useViewportWidth();
+ // Pull the played-card pile inward on narrow screens so it clears the side stacks.
+ const trickScale = vw < 360 ? 0.5 : vw < 460 ? 0.64 : 1;
const { phase, players, hakem, trump, turn, currentTrick } = game;
const legalIds = new Set(
@@ -149,12 +152,12 @@ export function GameTable({
{/* opponents' face-down hands */}
-
-
-
+
+
+
- {/* center trick area */}
-
+ {/* center trick area (offsets scale down on narrow screens) */}
+
@@ -273,7 +276,7 @@ function SeatAvatar({ seat, className }: { seat: Seat; className?: string }) {
const name = sp?.name ?? player.name;
return (
-
+
{trick.map((pc) => {
- const off = TRICK_OFFSET[pc.seat];
+ const off = { x: TRICK_OFFSET[pc.seat].x * scale, y: TRICK_OFFSET[pc.seat].y * scale };
const enter = TRICK_ENTER[pc.seat];
const isWinner =
phase === "trick-complete" && winner === pc.seat;
diff --git a/src/components/screens/LeaderboardScreen.tsx b/src/components/screens/LeaderboardScreen.tsx
index 352405c..8f7507f 100644
--- a/src/components/screens/LeaderboardScreen.tsx
+++ b/src/components/screens/LeaderboardScreen.tsx
@@ -3,9 +3,9 @@
import { useEffect } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
import { RankBadge } from "@/components/online/RankBadge";
+import { Avatar } from "@/components/online/Avatar";
import { useOnlineStore } from "@/lib/online-store";
import { useI18n } from "@/lib/i18n";
-import { avatarEmoji } from "@/lib/online/types";
import { cn } from "@/lib/cn";
const MEDALS: Record = { 1: "🥇", 2: "🥈", 3: "🥉" };
@@ -27,25 +27,45 @@ export function LeaderboardScreen() {
-
+
{MEDALS[e.rank] ?? e.rank}
- {avatarEmoji(e.avatar)}
+
+ {/* avatar with a level ring badge */}
+
+
{e.displayName}
{e.isYou && ({t("seat.you")})}
-
- {t("common.level")} {e.level}
+ {/* progress to next level */}
+
+
+
+ {t("common.level")} {e.level + 1}
+
+
))}
diff --git a/src/lib/online/mock-service.ts b/src/lib/online/mock-service.ts
index 4a05338..32ff87b 100644
--- a/src/lib/online/mock-service.ts
+++ b/src/lib/online/mock-service.ts
@@ -9,6 +9,7 @@ import {
STICKER_PACKS,
applyMatchResult,
dailyRewardFor,
+ xpNeededForLevel,
} from "./gamification";
import {
CreateRoomOptions,
@@ -820,14 +821,17 @@ export class MockOnlineService implements OnlineService {
avatar: pick(AVATARS).id,
level: randInt(5, 60),
rating: randInt(1000, 2200),
+ levelProgress: Math.random(),
isYou: false,
}));
const you = {
id: p.id,
displayName: p.displayName,
avatar: p.avatar,
+ avatarImage: p.avatarImage,
level: p.level,
rating: p.rating,
+ levelProgress: Math.min(1, p.xp / xpNeededForLevel(p.level)),
isYou: true,
};
const all = [...others, you].sort((a, b) => b.rating - a.rating);
diff --git a/src/lib/online/types.ts b/src/lib/online/types.ts
index 67f7e9e..700e3a8 100644
--- a/src/lib/online/types.ts
+++ b/src/lib/online/types.ts
@@ -364,8 +364,11 @@ export interface LeaderboardEntry {
id: string;
displayName: string;
avatar: string;
+ avatarImage?: string; // custom uploaded photo (overrides avatar)
level: number;
rating: number;
+ /** progress 0..1 toward the next level (for the XP bar) */
+ levelProgress: number;
isYou: boolean;
}