fix: center trick pile; add error boundary (surface post-buy crash)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 1m51s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m4s
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled

- Trick area: smaller offsets (±50/52) + retuned scale so the played pile sits
  centered in the felt instead of flung out to the side seats.
- ErrorBoundary around screens + overlays: a render error now shows a recoverable
  in-app message with the cause (and logs componentStack) instead of the browser's
  blank "page couldn't load" — helps pinpoint the post-purchase crash.

Verified: tsc + next build clean; web rebuilt on :1500.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 07:27:34 +03:30
parent 12177d2a33
commit b0668e6e31
3 changed files with 62 additions and 12 deletions
+3
View File
@@ -21,6 +21,7 @@ import { NotificationToaster } from "@/components/online/NotificationToaster";
import { ResumeGameBar } from "@/components/online/ResumeGameBar";
import { CelebrationOverlay } from "@/components/online/CelebrationOverlay";
import { MusicToggle } from "@/components/online/MusicToggle";
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { PublicProfileModal } from "@/components/online/PublicProfileModal";
import { CapacitorBack } from "@/components/CapacitorBack";
import { useSessionStore } from "@/lib/session-store";
@@ -193,6 +194,7 @@ export default function Page() {
// reducedMotion="user" makes every Framer Motion animation honor the OS
// "reduce motion" accessibility setting (coin rain, confetti, count-ups…).
<MotionConfig reducedMotion="user">
<ErrorBoundary>
{renderScreen(screen)}
<DailyRewardModal />
<NotificationToaster />
@@ -200,6 +202,7 @@ export default function Page() {
<CelebrationOverlay />
<MusicToggle />
<PublicProfileModal />
</ErrorBoundary>
<CapacitorBack />
{loading && null}
</MotionConfig>
+45
View File
@@ -0,0 +1,45 @@
"use client";
import React from "react";
/**
* Catches React render errors so a crash shows a recoverable in-app message
* (with the cause) instead of the browser's blank "page couldn't load" screen.
*/
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ error: Error | null }
> {
state: { error: Error | null } = { error: null };
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
// Surface the real cause in the console for diagnosis.
console.error("[Hokm] render error:", error, info?.componentStack);
}
render() {
if (!this.state.error) return this.props.children;
return (
<div className="fixed inset-0 z-[100] grid place-items-center bg-navy-950 p-6 text-center">
<div className="glass rounded-3xl p-6 w-full max-w-sm">
<div className="text-5xl mb-2"></div>
<h2 className="gold-text text-xl font-black">مشکلی پیش آمد</h2>
<p className="text-cream/60 text-sm mt-1">برنامه به مشکل خورد؛ دوباره بارگذاری کنید.</p>
<pre className="mt-3 max-h-32 overflow-auto rounded-lg bg-navy-900/70 p-2 text-start text-[10px] leading-relaxed text-rose-300/80 whitespace-pre-wrap break-words">
{String(this.state.error?.message || this.state.error)}
</pre>
<button
onClick={() => window.location.reload()}
className="btn-gold rounded-xl px-5 py-2.5 mt-4 font-bold"
>
بارگذاری مجدد
</button>
</div>
</div>
);
}
}
+7 -5
View File
@@ -65,7 +65,7 @@ export function GameTable({
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.6 : 1;
const trickScale = vw < 400 ? 0.82 : 1;
// Smaller played cards on phones so the center pile stays clear of the side seats.
const trickCardSize: "sm" | "md" = vw < 480 ? "sm" : "md";
const { phase, players, hakem, trump, turn, currentTrick } = game;
@@ -410,11 +410,13 @@ function OpponentHand({
/* ----------------------------- Trick area ----------------------------- */
// Compact, centered cross — small magnitudes keep the played pile in the middle of
// the felt (clear of the side seats/stacks). Each card still nudges toward its player.
const TRICK_OFFSET: Record<Seat, { x: number; y: number }> = {
0: { x: 0, y: 70 },
1: { x: 96, y: 0 },
2: { x: 0, y: -70 },
3: { x: -96, y: 0 },
0: { x: 0, y: 52 },
1: { x: 50, y: 0 },
2: { x: 0, y: -52 },
3: { x: -50, y: 0 },
};
const TRICK_ENTER: Record<Seat, { x: number; y: number }> = {
0: { x: 0, y: 260 },