| 1 | |
| 2 | |
| 3 | export type Suit = 'S' | 'H' | 'C' | 'D' | 'J'; |
| 4 | |
| 5 | |
| 6 | export interface Card { |
| 7 | id: number; |
| 8 | suit: Suit; |
| 9 | rank: number; |
| 10 | } |
| 11 | import type { CSSProperties, HTMLAttributes } from 'react'; |
| 12 | import { memo } from 'react'; |
| 13 | import { CourtIcon, JesterIcon, SuitIcon } from './Suit'; |
| 14 | |
| 15 | const FACE: Record<number, string> = { 11: 'J', 12: 'Q', 13: 'K', 14: 'A' }; |
| 16 | |
| 17 | export function rankText(rank: number): string { |
| 18 | return FACE[rank] ?? String(rank); |
| 19 | } |
| 20 | |
| 21 | export interface PlayingCardProps extends HTMLAttributes<HTMLDivElement> { |
| 22 | card?: Card | null; |
| 23 | |
| 24 | w?: string; |
| 25 | faceDown?: boolean; |
| 26 | selected?: boolean; |
| 27 | dim?: boolean; |
| 28 | highlight?: boolean; |
| 29 | |
| 30 | wild?: boolean; |
| 31 | } |
| 32 | |
| 33 | export const PlayingCard = memo(function PlayingCard({ |
| 34 | card, |
| 35 | w = 'var(--cw)', |
| 36 | faceDown, |
| 37 | selected, |
| 38 | dim, |
| 39 | highlight, |
| 40 | wild, |
| 41 | className = '', |
| 42 | style, |
| 43 | ...rest |
| 44 | }: PlayingCardProps) { |
| 45 | const cls = [ |
| 46 | 'pcard', |
| 47 | faceDown || !card ? 'pcard--back' : '', |
| 48 | selected ? 'is-selected' : '', |
| 49 | dim ? 'is-dim' : '', |
| 50 | highlight ? 'is-highlight' : '', |
| 51 | className, |
| 52 | ] |
| 53 | .filter(Boolean) |
| 54 | .join(' '); |
| 55 | const s = { '--w': w, ...style } as CSSProperties; |
| 56 | |
| 57 | if (faceDown || !card) { |
| 58 | return ( |
| 59 | <div className={cls} style={s} {...rest}> |
| 60 | <div className="pcard__back"> |
| 61 | <span className="pcard__emblem" /> |
| 62 | </div> |
| 63 | </div> |
| 64 | ); |
| 65 | } |
| 66 | |
| 67 | if (card.suit === 'J') { |
| 68 | const big = card.rank === 17; |
| 69 | return ( |
| 70 | <div className={`${cls} pcard--joker ${big ? 'is-big' : ''}`} style={s} {...rest}> |
| 71 | <div className="pcard__jindex">{big ? '大王' : '小王'}</div> |
| 72 | <JesterIcon className="pcard__jcenter" /> |
| 73 | </div> |
| 74 | ); |
| 75 | } |
| 76 | |
| 77 | const face = card.rank >= 11 && card.rank <= 13; |
| 78 | return ( |
| 79 | <div className={cls} style={s} data-suit={card.suit} {...rest}> |
| 80 | <div className="pcard__index"> |
| 81 | <span className={`pcard__rank ${card.rank === 10 ? 'is-ten' : ''}`}> |
| 82 | {rankText(card.rank)} |
| 83 | </span> |
| 84 | <SuitIcon suit={card.suit} className="pcard__suit" /> |
| 85 | </div> |
| 86 | {face ? ( |
| 87 | <CourtIcon rank={card.rank} className="pcard__court" /> |
| 88 | ) : ( |
| 89 | <SuitIcon suit={card.suit} className={`pcard__pip ${card.rank === 14 ? 'is-ace' : ''}`} /> |
| 90 | )} |
| 91 | {wild && <span className="pcard__wild">配</span>} |
| 92 | </div> |
| 93 | ); |
| 94 | }); |