Match intro "players joining" loading screen + i18n fix; checkpoint
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 7m38s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 1s

- MatchIntroOverlay: UNO-style pre-game reveal — the 4 seats animate into the
  table (with "?" placeholders until each player's data streams in for live
  matches), a 3-2-1-GO countdown, then the table shows. Wired via game-store
  matchIntroPending/consumeIntro, rendered online-only in GameScreen.
- Fix: intro.found / intro.getReady / intro.go existed only in the Persian dict;
  added the English strings (would have shown raw keys to EN users).
- Checkpoint of the in-progress UI/social batch (CoinsPill, shop titles section,
  friend-request rate limit, etc.) — all green.

Verified: tsc + next build + scripts/sim.ts + dotnet build server/Hokm.slnx all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-06 21:58:54 +03:30
parent cb27a16dc1
commit 03dfbe1e67
16 changed files with 319 additions and 79 deletions
+11 -1
View File
@@ -1,9 +1,10 @@
"use client";
import { motion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { GameTable } from "@/components/GameTable";
import { PostMatchRewardsModal } from "@/components/online/PostMatchRewardsModal";
import { MatchIntroOverlay } from "@/components/online/MatchIntroOverlay";
import { useGameStore } from "@/lib/game-store";
import { useSessionStore } from "@/lib/session-store";
import { useUIStore } from "@/lib/ui-store";
@@ -24,6 +25,8 @@ export function GameScreen() {
const forfeited = useGameStore((s) => s.forfeited);
const forfeitRequest = useGameStore((s) => s.forfeitRequest);
const respondForfeit = useGameStore((s) => s.respondForfeit);
const introPending = useGameStore((s) => s.matchIntroPending);
const consumeIntro = useGameStore((s) => s.consumeIntro);
const returnTo = useUIStore((s) => s.returnTo);
const go = useUIStore((s) => s.go);
const refreshProfile = useSessionStore((s) => s.refreshProfile);
@@ -139,6 +142,13 @@ export function GameScreen() {
onForfeit={canForfeit ? () => useGameStore.getState().forfeit() : undefined}
/>
{/* UNO-style "players joining the table" intro (online matches, once) */}
<AnimatePresence>
{introPending && mode === "online" && (
<MatchIntroOverlay onDone={consumeIntro} />
)}
</AnimatePresence>
{/* teammate asked to forfeit — confirm or decline */}
{forfeitRequest && (
<motion.div
+2 -9
View File
@@ -4,6 +4,7 @@ import { motion } from "framer-motion";
import { Coins, Lock, Trophy, Users } from "lucide-react";
import { useState } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
import { CoinsPill } from "@/components/online/CoinsPill";
import { MATCH_LEAGUES, leagueById } from "@/lib/online/gamification";
import { useOnlineStore } from "@/lib/online-store";
import { useSessionStore } from "@/lib/session-store";
@@ -43,15 +44,7 @@ export function OnlineLobbyScreen() {
return (
<ScreenShell>
<ScreenHeader
title={t("lobby.title")}
right={
<span className="glass rounded-full px-3 py-1.5 text-xs font-bold text-gold-300 flex items-center gap-1">
<Coins className="size-3.5 text-gold-400" />
{coins.toLocaleString()}
</span>
}
/>
<ScreenHeader title={t("lobby.title")} right={<CoinsPill />} />
{/* league pick (only for ranked) */}
<div className="glass rounded-2xl p-4 mb-4">
+3 -5
View File
@@ -1,10 +1,11 @@
"use client";
import { motion } from "framer-motion";
import { Check, ChevronLeft, Coins, Crown, Eye, EyeOff, Lock, Music, Pencil, Star, Upload, Users, Volume2 } from "lucide-react";
import { Check, ChevronLeft, Crown, Eye, EyeOff, Lock, Music, Pencil, Star, Upload, Users, Volume2 } from "lucide-react";
import { useRef, useState } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
import { RankBadge } from "@/components/online/RankBadge";
import { CoinsPill } from "@/components/online/CoinsPill";
import { XpBar } from "@/components/online/XpBar";
import { Avatar } from "@/components/online/Avatar";
import { useSessionStore } from "@/lib/session-store";
@@ -138,10 +139,7 @@ export function ProfileScreen() {
<div className="mt-2 flex items-center justify-center gap-2">
<RankBadge rating={profile.rating} showRating />
<span className="glass rounded-full px-2.5 py-1 text-xs font-bold text-gold-300 flex items-center gap-1">
<Coins className="size-3.5 text-gold-400" />
{profile.coins.toLocaleString()}
</span>
<CoinsPill />
</div>
<div className="mt-4">
+2 -9
View File
@@ -5,6 +5,7 @@ import { Check, Coins, Sparkles, X } from "lucide-react";
import { useEffect, useState } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
import { Sticker } from "@/components/online/Sticker";
import { CoinsPill } from "@/components/online/CoinsPill";
import { useSessionStore } from "@/lib/session-store";
import { useI18n } from "@/lib/i18n";
import { getService } from "@/lib/online/service";
@@ -146,15 +147,7 @@ export function ShopScreen() {
return (
<ScreenShell>
<ScreenHeader
title={t("shop.title")}
right={
<span className="glass rounded-full px-3 py-1.5 text-xs font-bold text-gold-300 flex items-center gap-1">
<Coins className="size-3.5 text-gold-400" />
{profile.coins.toLocaleString()}
</span>
}
/>
<ScreenHeader title={t("shop.title")} right={<CoinsPill />} />
{msg && (
<div className="mb-3 text-center text-rose-300 text-sm glass rounded-xl py-2">{msg}</div>