Files
HokmPlay/src/components/PlayingCard.tsx
T

152 lines
5.0 KiB
TypeScript
Raw Normal View History

"use client";
import { cn } from "@/lib/cn";
import { Card, SUIT_IS_RED, SUIT_SYMBOL, rankLabel } from "@/lib/hokm/types";
import type { CardBackPattern } from "@/lib/online/types";
import { cardBackMotif, cardBackVisual } from "@/lib/cardBack";
const SIZES = {
sm: { w: 44, h: 62, rank: "text-[11px]", center: "text-2xl", radius: 7 },
md: { w: 62, h: 87, rank: "text-sm", center: "text-3xl", radius: 9 },
lg: { w: 78, h: 110, rank: "text-base", center: "text-4xl", radius: 11 },
xl: { w: 92, h: 130, rank: "text-lg", center: "text-5xl", radius: 13 },
} as const;
export type CardSize = keyof typeof SIZES;
interface CardBack { c1: string; c2: string; accent: string; pattern?: CardBackPattern; motif?: string; }
interface CardFront { bg1: string; bg2: string; border: string; }
interface Props {
card?: Card;
faceDown?: boolean;
size?: CardSize;
className?: string;
dimmed?: boolean;
back?: CardBack;
front?: CardFront;
}
export function PlayingCard({
card,
faceDown,
size = "md",
className,
dimmed,
back,
front,
}: Props) {
const s = SIZES[size];
/* ── Face-down ─────────────────────────────────────────────────── */
if (faceDown || !card) {
const visual = back ? cardBackVisual(back.c1, back.c2, back.accent, back.pattern) : null;
const styled = back && visual ? {
width: s.w, height: s.h, borderRadius: s.radius,
background: visual.background,
backgroundSize: visual.backgroundSize,
border: `1.5px solid ${back.accent}88`,
boxShadow: "0 6px 18px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.12)",
} : { width: s.w, height: s.h };
const motif = back ? cardBackMotif(back.pattern, back.motif) : "✦";
return (
<div
className={cn(!back && "card-back", "shrink-0", className)}
style={{ ...styled, borderRadius: s.radius }}
aria-hidden
>
<div
className="h-full w-full flex items-center justify-center"
style={{ borderRadius: s.radius }}
>
{motif && (
<span
className={cn("font-bold select-none", !back && "text-gold-500/70")}
style={{
fontSize: s.w * 0.34,
color: back ? `${back.accent}dd` : undefined,
textShadow: back ? `0 1px 3px rgba(0,0,0,0.45)` : undefined,
}}
>
{motif}
</span>
)}
</div>
</div>
);
}
/* ── Face-up ────────────────────────────────────────────────────── */
const red = SUIT_IS_RED[card.suit];
const symbol = SUIT_SYMBOL[card.suit];
const label = rankLabel(card.rank);
// UNO-style: suit-aware background
const cardBg = front
? `linear-gradient(160deg,${front.bg1},${front.bg2})`
: red
? "linear-gradient(160deg,#fff8f7,#fdecea)"
: "linear-gradient(160deg,#fefefe,#f4f2ec)";
const borderColor = front?.border ?? (red ? "rgba(200,70,70,0.22)" : "rgba(50,50,80,0.15)");
// Bold suit colours (UNO-style vivid)
const inkColor = red ? "#c0202a" : "#1c1c38";
const pipColor = red ? "#e03540" : "#2a2a50";
return (
<div
className={cn("shrink-0 relative select-none transition-opacity", dimmed && "opacity-40", className)}
style={{
width: s.w, height: s.h, borderRadius: s.radius,
background: cardBg,
border: `1.5px solid ${borderColor}`,
boxShadow: "0 6px 18px rgba(0,0,0,0.38), inset 0 1px 0 rgba(255,255,255,0.85)",
}}
>
{/* Top-left corner */}
<div
className={cn("absolute top-[3px] left-[5px] leading-[1.1] font-black", s.rank)}
style={{ color: inkColor }}
>
<div>{label}</div>
<div style={{ color: pipColor, fontSize: "0.82em" }}>{symbol}</div>
</div>
{/* Center symbol — large, bold, slightly shadowed */}
<div
className={cn("absolute inset-0 flex items-center justify-center font-black", s.center)}
style={{
color: inkColor,
textShadow: red
? "0 2px 10px rgba(210,40,40,0.18)"
: "0 2px 10px rgba(28,28,56,0.12)",
}}
>
{symbol}
</div>
{/* Bottom-right corner (rotated 180°) */}
<div
className={cn("absolute bottom-[3px] right-[5px] leading-[1.1] font-black rotate-180", s.rank)}
style={{ color: inkColor }}
>
<div>{label}</div>
<div style={{ color: pipColor, fontSize: "0.82em" }}>{symbol}</div>
</div>
{/* Subtle inner rim for red suits — UNO-style */}
{red && (
<div
className="absolute inset-[3px] pointer-events-none"
style={{
borderRadius: Math.max(0, s.radius - 4),
border: "1px solid rgba(210,40,40,0.14)",
}}
/>
)}
</div>
);
}