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.level} + +
+
{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; }