<div id="root">
    <!-- This element's contents will be replaced with your component. -->
</div>
* {
  box-sizing: border-box;
}

:root {
  --primary: #333;
  --secondary: #eee;
}

body {
  margin: 0;
  font-family: Arial;
  background: #35654d;
  background: linear-gradient(to right, #348f50, #35654d);
  color: var(--primary);
  letter-spacing: 1px;
  text-align: center;
}

#root {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

h1 {
  color: var(--secondary);
}

main {
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  align-items: center;
}

ul {
  list-style-type: none;
  padding: 0;
}

select {
    appearance: none;
    text-transform: none;
    font-family: inherit;
    font-size: 100%;
    letter-spacing: inherit;
    line-height: 1.15;
    color: #333;
    border: 1px solid var(--primary);
    border-radius: 4px;
    background: var(--secondary);
    padding: .25rem .5rem;
    cursor: pointer;
    outline: none;
    transition: all 0.2s linear;
  
    &:hover, &:focus {
      border-color: #888;
      color: var(--secondary);
      background: var(--primary);
    }
}

button {
  -webkit-appearance: button;
  text-transform: none;
  font-family: inherit;
  font-size: 100%;
  letter-spacing: inherit;
  line-height: 1.15;
  overflow: visible;
  color: var(--primary);
  border: 1px solid var(--primary);
  border-radius: 4px;
  background: var(--secondary);
  padding: .25rem .5rem;
  cursor: pointer;
  outline: none;
  transition: all 0.2s linear;
  
  &:hover, &:focus {
    color: var(--secondary);
    background: var(--primary);
  }
}

.btns {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
  justify-content: center;
}
  
.table {
  margin-bottom: 1rem;
  width: 90%;
  padding: 2rem;
  border-radius: 8px;
  
  h2 {
    margin-top: 0;
  }
  
  &.dark {
    color: var(--secondary);
    background: rgba(0,0,0,0.5);
    
    .card {
      background: rgba(0,0,0,0.25);
    }
  }
  
  &.light {
    color: var(--primary);
    background: rgba(256,256,256,0.5);
    
    .card {
      background: rgba(256,256,256,0.25);
    }
  }
}

.cards {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  min-height: 9rem;
  transition: all 0.5s ease;
}

.card {
  width: 6rem;
  height: 9rem;
  border: solid 1px;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 3rem;
  backface-visibility: hidden;
  transition: all 0.5s ease;
  animation: card-enter 1s ease-out;
}

@keyframes card-enter {
  0% {
    opacity: 0;
    transform: scaleX(0) translateY(-50px) rotateY(-180deg);
  }
  100% {
    opacity: 1;
    transform: scaleX(1) translateY(0) rotateY(0deg);
  }
}

main > p {
  color: var(--secondary);
}

footer {
  margin-top: auto;
  color: var(--secondary);
  font-size: 0.875rem;
  
  p {
    margin: 1rem 0;
  }
  
  a {
    color: inherit;
    opacity: 0.5;
    text-decoration: none;
    &:hover {
      opacity: 1;
      text-decoration: underline;
    }
  }
}
View Compiled
const { useState, useEffect, useRef } = React;
const { createRoot } = ReactDOM;

// ADD ANIMATIONS
// ADAPT RULES TO REAL Blackjack

type CardType = {
  id: string;
  card: string;
  color: string;
}

const ACE = ['A'];
const NUMS = ['2', '3', '4', '5', '6', '7', '8', '9', '10'];
const FIGS = ['J', 'Q', 'K'];
const CARDS = [...ACE, ...NUMS, ...FIGS];
const COLORS = ['♠', '♥', '♣', '♦'];

const DECK: CardType[] = CARDS.map(card => {
  return COLORS.map(color => ({
    id: `${card}${color}`,
    card: card,
    color: color,
  }));
}).flat();

const checkDuplicate = (index: number, cards: CardType[]): boolean => {
  return cards.includes(DECK[index]);
};

const getSumsk = (cards: CardType[], isHigh: boolean): number => {
  return cards?.reduce((acc, cur) => {
    if (NUMS.includes(cur.card)) return acc + Number(cur.card);
    if (ACE.includes(cur.card)) return acc + (isHigh ? 11 : 1);
    if (FIGS.includes(cur.card)) return acc + 10;
  }, 0);
};

const getSum = (cards: CardType[]): number => {
  const high = getSumsk(cards, true);
  const low = getSumsk(cards, false);
  return high > 21 ? low : high;
};

const delay = (fn: () => void, ms: number) => {
  setTimeout(() => { fn() }, ms);
};

const Scores = ({ tryAgain, turn, money }: { tryAgain: () => void; turn: number; money: number }) => {
  const [scores, setScores] = useState([]);

  useEffect(() => {
    const storedScores = localStorage.getItem('scores');
    if (storedScores) {
      setScores(JSON.parse(storedScores));
    }
  }, []);
  
  const saveScore = () => {
    const updatedScores = [...scores, money];
    setScores(updatedScores);
    localStorage.setItem('scores', JSON.stringify(updatedScores));
    tryAgain();
  }
  
  return (
    <div className="table light">
      <h2>Scores</h2>
      {turn > 0 ? <button onClick={saveScore}>Stop & save score</button> : null}
      {scores?.length ? (
        <ul>
          {scores.map((score, index) => (
            <li key={index}>{score}</li>
          ))}
        </ul>
      ) : <p>No scores yet.</p>}
    </div>
  )
}

const Start = ({ status, handleStart, money, tryAgain, bet, setBet }: { status: string; handleStart: () => void; money: number; tryAgain: () => void; bet: number; setBet: (bet: number) => void }) => {
  const titles = {
    start: 'Try and defeat the bank!',
    win: 'You win! Congratulations!',
    lose: 'You lose! Give me the money!',
  };

  return (
    <div className='table light'>
      <h2>{titles[status]}</h2>
      {money ? (<div className="btns">
        <select value={bet} onChange={(e)=> setBet(e.target.value)}>
          {Array.from({ length: money / 10 }, (_, i) => (i + 1) * 10).map((x) => (
            <option key={x} value={x}>
              {x}
            </option>
          ))}
        </select>
        <button onClick={handleStart}>
          {status === 'start' ? 'Bet' : 'Bet again'}
        </button>
      </div>) :
      <button onClick={tryAgain}>Try again</button>}
    </div>
  );
};

const Bank = ({ cards }: { cards: CardType[] }) => {
  return (
    <div className='table dark'>
      <h2>Bank {getSum(cards)}</h2>

      <div className='cards'>
        {cards.map((c) => (
          <div key={c.id} className='card'>{c.id}</div>
        ))}
      </div>
    </div>
  );
};

const Player = ({ cards }: { cards: CardType[] }) => {
  return (
    <div className='table light'>
      <h2>Player {getSum(cards)}</h2>

      <div className='cards'>
        {cards.map((c) => (
          <div key={c.id} className='card'>{c.id}</div>
        ))}
      </div>
    </div>
  );
};

const App = () => {
  const [status, setStatus] = useState('start');
  const [finish, setFinish] = useState(false);
  const [bankCards, setBankCards] = useState<CardType[]>([]);
  const [playerCards, setPlayerCards] = useState<CardType[]>([]);
  const [money, setMoney] = useState(100);
  const [bet, setBet] = useState(10);
  const [turn, setTurn] = useState(0);

  const pSum = getSum(playerCards);
  const bSum = getSum(bankCards);

  const maxDeal = pSum === 21;
  const gameOver = pSum > 21;

  const getCard = (): CardType => {
    let randomIndex = Math.floor(Math.random() * DECK.length);
    while (checkDuplicate(randomIndex, [...playerCards, ...bankCards])) {
      randomIndex = Math.floor(Math.random() * DECK.length);
    }
    return DECK[randomIndex];
  };

  const handleStart = () => {
    setStatus('');
    setFinish(false);
    setTurn(c => c + 1);
    setMoney(c => c - bet);
    // delay(() => setBankCards([getCard()]), 500);
    setBankCards([getCard()]);
    setPlayerCards([getCard()]);
    delay(() => setPlayerCards(c => [...c, getCard()]), 500);
    // setPlayerCards([getCard(), getCard()]);
  };

  const handleFinish = () => {
    setFinish(true);
    setBankCards(c => [...c, getCard()]);
  };

  const handleDeal = () => {
    setPlayerCards(c => [...c, getCard()]);
  };
  
  const tryAgain = () => {
    setMoney(100);
    setTurn(0);
    setStatus("start");
  }

  const winOrLose = (isWin: boolean) => {
    setBet(10);
    delay(() => setStatus(isWin ? 'win' : 'lose'), 1000);
  };

  useEffect(() => {
    if (finish) {
      if (bSum < pSum) {
        delay(() => setBankCards(c => [...c, getCard()]), 500);
      } else if (bSum <= 21) {
        winOrLose(false);
      } else {
        setMoney(c => c + bet * 2);
        winOrLose(true);
      }
    }
  }, [bankCards]);

  useEffect(() => {
    if (gameOver) {
      winOrLose(false);
    }
  }, [playerCards]);

  return (
    <>
      <h1>Black Jack <small>({money}$)</small></h1>

      <main>
        {!!status ? (
          <>
            <Start status={status} handleStart={handleStart} tryAgain={tryAgain} money={money} bet={bet} setBet={setBet} />
            <Scores tryAgain={tryAgain} turn={turn} money={money} />
          </>
        ) : (
          <>
            <Bank cards={bankCards} />
            <Player cards={playerCards} />
            {!finish && !gameOver ? (
              <div className='btns'>
                {!maxDeal ? <button onClick={handleDeal}>Deal</button> : null}
                <button onClick={handleFinish}>Finish</button>
                {/*<button onClick={() => setStatus("start")}>Cancel</button>*/}
              </div>
            ) : null}
          </>
        )}
        <p>Turn: {turn}</p>
      </main>

      <footer>
        <p>Created by <a href='https://remybeumier.be' target='_blank'>Rémy Beumier</a></p>
      </footer>
    </>
  );
};

const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/react@18.3.1/umd/react.production.min.js
  2. https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js
  3. https://unpkg.com/@types/react/index.d.ts
  4. https://unpkg.com/@types/react-dom/index.d.ts