diff --git a/frontend/public/cards/suit-icons/acorn-icon@large.png b/frontend/public/cards/suit-icons/acorn-icon@large.png new file mode 100644 index 0000000..99eea2a Binary files /dev/null and b/frontend/public/cards/suit-icons/acorn-icon@large.png differ diff --git a/frontend/public/cards/suit-icons/acorn-icon@medium.png b/frontend/public/cards/suit-icons/acorn-icon@medium.png new file mode 100644 index 0000000..d71fb53 Binary files /dev/null and b/frontend/public/cards/suit-icons/acorn-icon@medium.png differ diff --git a/frontend/public/cards/suit-icons/acorn-icon@small.png b/frontend/public/cards/suit-icons/acorn-icon@small.png new file mode 100644 index 0000000..7de1ab2 Binary files /dev/null and b/frontend/public/cards/suit-icons/acorn-icon@small.png differ diff --git a/frontend/public/cards/suit-icons/bell-icon@large.png b/frontend/public/cards/suit-icons/bell-icon@large.png new file mode 100644 index 0000000..83a1519 Binary files /dev/null and b/frontend/public/cards/suit-icons/bell-icon@large.png differ diff --git a/frontend/public/cards/suit-icons/bell-icon@medium.png b/frontend/public/cards/suit-icons/bell-icon@medium.png new file mode 100644 index 0000000..6f4db9b Binary files /dev/null and b/frontend/public/cards/suit-icons/bell-icon@medium.png differ diff --git a/frontend/public/cards/suit-icons/bell-icon@small.png b/frontend/public/cards/suit-icons/bell-icon@small.png new file mode 100644 index 0000000..2b12990 Binary files /dev/null and b/frontend/public/cards/suit-icons/bell-icon@small.png differ diff --git a/frontend/public/cards/suit-icons/heart-icon@large.png b/frontend/public/cards/suit-icons/heart-icon@large.png new file mode 100644 index 0000000..d51a00a Binary files /dev/null and b/frontend/public/cards/suit-icons/heart-icon@large.png differ diff --git a/frontend/public/cards/suit-icons/heart-icon@medium.png b/frontend/public/cards/suit-icons/heart-icon@medium.png new file mode 100644 index 0000000..cbee200 Binary files /dev/null and b/frontend/public/cards/suit-icons/heart-icon@medium.png differ diff --git a/frontend/public/cards/suit-icons/heart-icon@small.png b/frontend/public/cards/suit-icons/heart-icon@small.png new file mode 100644 index 0000000..a4c2be9 Binary files /dev/null and b/frontend/public/cards/suit-icons/heart-icon@small.png differ diff --git a/frontend/public/cards/suit-icons/leaf-icon@large.png b/frontend/public/cards/suit-icons/leaf-icon@large.png new file mode 100644 index 0000000..1587b2f Binary files /dev/null and b/frontend/public/cards/suit-icons/leaf-icon@large.png differ diff --git a/frontend/public/cards/suit-icons/leaf-icon@medium.png b/frontend/public/cards/suit-icons/leaf-icon@medium.png new file mode 100644 index 0000000..65f8ff2 Binary files /dev/null and b/frontend/public/cards/suit-icons/leaf-icon@medium.png differ diff --git a/frontend/public/cards/suit-icons/leaf-icon@small.png b/frontend/public/cards/suit-icons/leaf-icon@small.png new file mode 100644 index 0000000..3db54ae Binary files /dev/null and b/frontend/public/cards/suit-icons/leaf-icon@small.png differ diff --git a/frontend/src/components/CardSkinToggle.tsx b/frontend/src/components/CardSkinToggle.tsx new file mode 100644 index 0000000..b313ba3 --- /dev/null +++ b/frontend/src/components/CardSkinToggle.tsx @@ -0,0 +1,23 @@ +import { useCardSkin } from '../lib/cardSkin'; + +/** Small header button that flips the deck between the classic vector cards and + * the illustrated "sedmové" art. Persists via the cardSkin store. */ +export default function CardSkinToggle({ className = '' }: { className?: string }) { + const skin = useCardSkin((s) => s.skin); + const toggle = useCardSkin((s) => s.toggle); + const illustrated = skin === 'illustrated'; + + return ( + + ); +} diff --git a/frontend/src/components/CardView.tsx b/frontend/src/components/CardView.tsx index 71fc8c7..08e8329 100644 --- a/frontend/src/components/CardView.tsx +++ b/frontend/src/components/CardView.tsx @@ -1,4 +1,5 @@ import type { Card } from '../types'; +import { useCardSkin, suitIcon } from '../lib/cardSkin'; const SUIT_SYMBOL: Record = { HEARTS: '♥', @@ -19,6 +20,15 @@ const VALUE_LABEL: Record = { LOWER: 'J', UPPER: 'Q', KING: 'K', ACE: 'A', }; +// The bell, leaf and heart icons are a touch taller than the acorn, so in the +// centre they sometimes bleed into the corner marks — nudge those three down. +const CENTER_ICON_SCALE: Record = { + HEARTS: 0.85, + LEAVES: 0.85, + BELLS: 0.85, + ACORNS: 1, +}; + // Per-size geometry. sm/md sit on the table, lg/xl are hand cards. const DIMS = { sm: { w: 38, h: 54, radius: 4, inset: 3, label: 9, suitSm: 7, suitLg: 18 }, @@ -37,26 +47,45 @@ interface Props { } export default function CardView({ card, onClick, disabled = false, highlight = false, size = 'md' }: Props) { + const skin = useCardSkin((s) => s.skin); const symbol = SUIT_SYMBOL[card.color]; const color = SUIT_COLOR[card.color]; const label = VALUE_LABEL[card.value]; const d = DIMS[size]; const interactive = !disabled && !!onClick; + // 'illustrated' skin keeps the classic card but swaps the ♥♠♣♦ glyphs for the + // suit-icon artwork; 'classic' uses the plain text glyphs. + const useIcons = skin === 'illustrated'; + // With the icon artwork the bell suit reads brown, so tint its value label to + // match instead of the classic blue. + const labelColor = useIcons && card.color === 'BELLS' ? '#8a5a1c' : color; const corner = (rotated: boolean) => ( - + {label} - {symbol} + {useIcons ? ( + {symbol} + ) : ( + {symbol} + )} ); @@ -76,7 +105,17 @@ export default function CardView({ card, onClick, disabled = false, highlight = > {corner(false)} - {symbol} + {useIcons ? ( + {symbol} + ) : ( + {symbol} + )} {corner(true)} diff --git a/frontend/src/lib/cardSkin.ts b/frontend/src/lib/cardSkin.ts new file mode 100644 index 0000000..f1a2353 --- /dev/null +++ b/frontend/src/lib/cardSkin.ts @@ -0,0 +1,58 @@ +import { create } from 'zustand'; +import type { CardColor } from '../types'; + +// Two ways to draw the suit marks on the (otherwise identical) cards: plain +// ♥♠♣♦ text glyphs ('classic') or the "sedmové" suit-icon artwork ('illustrated'). +export type CardSkin = 'classic' | 'illustrated'; + +const STORAGE_KEY = 'bridzik.cardSkin'; + +function loadSkin(): CardSkin { + try { + return localStorage.getItem(STORAGE_KEY) === 'illustrated' ? 'illustrated' : 'classic'; + } catch { + return 'classic'; + } +} + +interface CardSkinStore { + skin: CardSkin; + setSkin: (skin: CardSkin) => void; + toggle: () => void; +} + +export const useCardSkin = create((set) => ({ + skin: loadSkin(), + setSkin: (skin) => { + try { + localStorage.setItem(STORAGE_KEY, skin); + } catch { + /* ignore — a private-mode failure just means the choice isn't persisted */ + } + set({ skin }); + }, + toggle: () => set((s) => { + const skin: CardSkin = s.skin === 'illustrated' ? 'classic' : 'illustrated'; + try { + localStorage.setItem(STORAGE_KEY, skin); + } catch { + /* ignore */ + } + return { skin }; + }), +})); + +// Filenames in /public/cards use their own suit/value words, not the engine's +// enum names — map between the two here. +const SUIT_FILE: Record = { + HEARTS: 'heart', + LEAVES: 'leaf', + ACORNS: 'acorn', + BELLS: 'bell', +}; + +/** Path to a suit icon at a given resolution, e.g. `/cards/suit-icons/heart-icon@small.png`. + * Used to render the suit marks on the classic vector cards. */ +export function suitIcon(color: CardColor, size: 'small' | 'medium' | 'large'): string { + return `/cards/suit-icons/${SUIT_FILE[color]}-icon@${size}.png`; +} diff --git a/frontend/src/pages/GameTable.tsx b/frontend/src/pages/GameTable.tsx index 7f0df88..b09943f 100644 --- a/frontend/src/pages/GameTable.tsx +++ b/frontend/src/pages/GameTable.tsx @@ -8,12 +8,14 @@ import { computeTotal } from '../lib/standings'; import { displayName } from '../lib/names'; import { useIsDesktop } from '../lib/useIsDesktop'; import { useFitScale } from '../lib/useFitScale'; +import { SEAT_SIZE, TOTALS_FONT } from '../lib/boardTheme'; import Hand from '../components/Hand'; import GuessControls from '../components/GuessControls'; import Trick from '../components/Trick'; import Standings from '../components/Standings'; import PlayerCircle from '../components/PlayerCircle'; import FaceDownCards from '../components/FaceDownCards'; +import CardSkinToggle from '../components/CardSkinToggle'; import GameOver from './GameOver'; import type { Hand as HandCards, PlayerInfo, StashData } from '../types'; @@ -205,19 +207,19 @@ export default function GameTable() {
{opponents.map((p) => (
-
+
{displayName(p.name)}
-
+
{computeTotal(standings, p.order)}
))}
-
+
{myPlayer.name}
-
+
{computeTotal(standings, myOrder)}
@@ -241,27 +243,30 @@ export default function GameTable() { myOrder={myOrder} activePlayer={active_player} activePlayerName={activePlayerName} + desktop={desktop} /> ) : null ); + const seatSize = desktop ? SEAT_SIZE.desktop : SEAT_SIZE.mobile; + const topSeat = (
- +
); const sideSeat = (p?: PlayerInfo) => (
- +
); const meSeat = (
- +
); @@ -283,22 +288,23 @@ export default function GameTable() {
{/* header */}
- + Bridžik
- + Séria {series_number + 1} · Kolo {round_number + 1}
{banner}
{totalsRow(true)}
+ {canEnd && ( - )} -
@@ -344,16 +350,17 @@ export default function GameTable() { {/* header */}
- + Séria {series_number + 1} · Kolo {round_number + 1}
+ {canEnd && ( - )} -