feat(blackjack): add action indicators and loading states
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
	
		
	
		
			Some checks failed
		
		
	
	
This commit is contained in:
		
					parent
					
						
							
								c9f71f70fa
							
						
					
				
			
			
				commit
				
					
						adc5bbd345
					
				
			
		
					 7 changed files with 208 additions and 27 deletions
				
			
		| 
						 | 
					@ -5,10 +5,22 @@
 | 
				
			||||||
    <div class="lg:col-span-3 space-y-6 flex flex-col gap-4">
 | 
					    <div class="lg:col-span-3 space-y-6 flex flex-col gap-4">
 | 
				
			||||||
      <app-dealer-hand [cards]="dealerCards()"></app-dealer-hand>
 | 
					      <app-dealer-hand [cards]="dealerCards()"></app-dealer-hand>
 | 
				
			||||||
      <app-player-hand [cards]="playerCards()"></app-player-hand>
 | 
					      <app-player-hand [cards]="playerCards()"></app-player-hand>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <!-- Action message indicator -->
 | 
				
			||||||
 | 
					      @if (isActionInProgress()) {
 | 
				
			||||||
 | 
					        <div class="flex justify-center">
 | 
				
			||||||
 | 
					          <div class="card p-4 flex items-center gap-3 animate-pulse bg-deep-blue-light border border-deep-blue-light/50">
 | 
				
			||||||
 | 
					            <div class="w-5 h-5 rounded-full border-2 border-white border-t-transparent animate-spin"></div>
 | 
				
			||||||
 | 
					            <span>{{ currentAction() }}</span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      @if (gameInProgress()) {
 | 
					      @if (gameInProgress()) {
 | 
				
			||||||
        <app-game-controls
 | 
					        <app-game-controls
 | 
				
			||||||
          [playerCards]="playerCards()"
 | 
					          [playerCards]="playerCards()"
 | 
				
			||||||
          [gameState]="gameState()"
 | 
					          [gameState]="gameState()"
 | 
				
			||||||
 | 
					          [isActionInProgress]="isActionInProgress()"
 | 
				
			||||||
          (hit)="onHit()"
 | 
					          (hit)="onHit()"
 | 
				
			||||||
          (stand)="onStand()"
 | 
					          (stand)="onStand()"
 | 
				
			||||||
          (doubleDown)="onDoubleDown()"
 | 
					          (doubleDown)="onDoubleDown()"
 | 
				
			||||||
| 
						 | 
					@ -22,6 +34,7 @@
 | 
				
			||||||
        [balance]="balance()"
 | 
					        [balance]="balance()"
 | 
				
			||||||
        [currentBet]="currentBet()"
 | 
					        [currentBet]="currentBet()"
 | 
				
			||||||
        [gameInProgress]="gameInProgress()"
 | 
					        [gameInProgress]="gameInProgress()"
 | 
				
			||||||
 | 
					        [isActionInProgress]="isActionInProgress()"
 | 
				
			||||||
        (newGame)="onNewGame($event)"
 | 
					        (newGame)="onNewGame($event)"
 | 
				
			||||||
      ></app-game-info>
 | 
					      ></app-game-info>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,10 @@ export default class BlackjackComponent {
 | 
				
			||||||
  gameInProgress = signal(false);
 | 
					  gameInProgress = signal(false);
 | 
				
			||||||
  gameState = signal<string>('IN_PROGRESS');
 | 
					  gameState = signal<string>('IN_PROGRESS');
 | 
				
			||||||
  showGameResult = signal(false);
 | 
					  showGameResult = signal(false);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Add loading state trackers
 | 
				
			||||||
 | 
					  isActionInProgress = signal(false);
 | 
				
			||||||
 | 
					  currentAction = signal<string>('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    this.refreshUserBalance();
 | 
					    this.refreshUserBalance();
 | 
				
			||||||
| 
						 | 
					@ -89,64 +93,85 @@ export default class BlackjackComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onNewGame(bet: number): void {
 | 
					  onNewGame(bet: number): void {
 | 
				
			||||||
 | 
					    this.isActionInProgress.set(true);
 | 
				
			||||||
 | 
					    this.currentAction.set('Spiel wird gestartet...');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    this.blackjackService.startGame(bet).subscribe({
 | 
					    this.blackjackService.startGame(bet).subscribe({
 | 
				
			||||||
      next: (game) => {
 | 
					      next: (game) => {
 | 
				
			||||||
        this.updateGameState(game);
 | 
					        this.updateGameState(game);
 | 
				
			||||||
        this.refreshUserBalance();
 | 
					        this.refreshUserBalance();
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      error: (error) => {
 | 
					      error: (error) => {
 | 
				
			||||||
        console.error('Failed to start game:', error);
 | 
					        console.error('Failed to start game:', error);
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onHit(): void {
 | 
					  onHit(): void {
 | 
				
			||||||
    if (!this.currentGameId()) return;
 | 
					    if (!this.currentGameId() || this.isActionInProgress()) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.isActionInProgress.set(true);
 | 
				
			||||||
 | 
					    this.currentAction.set('Karte wird gezogen...');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    this.blackjackService.hit(this.currentGameId()!).subscribe({
 | 
					    this.blackjackService.hit(this.currentGameId()!).subscribe({
 | 
				
			||||||
      next: (game) => {
 | 
					      next: (game) => {
 | 
				
			||||||
        this.updateGameState(game);
 | 
					        this.updateGameState(game);
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      error: (error) => {
 | 
					      error: (error) => {
 | 
				
			||||||
        console.error('Failed to hit:', error);
 | 
					        console.error('Failed to hit:', error);
 | 
				
			||||||
        this.handleGameError(error);
 | 
					        this.handleGameError(error);
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onStand(): void {
 | 
					  onStand(): void {
 | 
				
			||||||
    if (!this.currentGameId()) return;
 | 
					    if (!this.currentGameId() || this.isActionInProgress()) return;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (this.gameState() !== 'IN_PROGRESS') {
 | 
					    if (this.gameState() !== 'IN_PROGRESS') {
 | 
				
			||||||
      console.log('Cannot stand: game is not in progress');
 | 
					      console.log('Cannot stand: game is not in progress');
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.isActionInProgress.set(true);
 | 
				
			||||||
 | 
					    this.currentAction.set('Dealer zieht Karten...');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    this.blackjackService.stand(this.currentGameId()!).subscribe({
 | 
					    this.blackjackService.stand(this.currentGameId()!).subscribe({
 | 
				
			||||||
      next: (game) => {
 | 
					      next: (game) => {
 | 
				
			||||||
        this.updateGameState(game);
 | 
					        this.updateGameState(game);
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      error: (error) => {
 | 
					      error: (error) => {
 | 
				
			||||||
        console.error('Failed to stand:', error);
 | 
					        console.error('Failed to stand:', error);
 | 
				
			||||||
        this.handleGameError(error);
 | 
					        this.handleGameError(error);
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onDoubleDown(): void {
 | 
					  onDoubleDown(): void {
 | 
				
			||||||
    if (!this.currentGameId()) return;
 | 
					    if (!this.currentGameId() || this.isActionInProgress()) return;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (this.gameState() !== 'IN_PROGRESS' || this.playerCards().length !== 2) {
 | 
					    if (this.gameState() !== 'IN_PROGRESS' || this.playerCards().length !== 2) {
 | 
				
			||||||
 | 
					      console.log('Cannot double down: game is not in progress or more than 2 cards');
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.isActionInProgress.set(true);
 | 
				
			||||||
 | 
					    this.currentAction.set('Einsatz wird verdoppelt...');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
 | 
					    this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
 | 
				
			||||||
      next: (game) => {
 | 
					      next: (game) => {
 | 
				
			||||||
        this.updateGameState(game);
 | 
					        this.updateGameState(game);
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      error: (error) => {
 | 
					      error: (error) => {
 | 
				
			||||||
        console.error('Failed to double down:', error);
 | 
					        console.error('Failed to double down:', error);
 | 
				
			||||||
        this.handleGameError(error);
 | 
					        this.handleGameError(error);
 | 
				
			||||||
 | 
					        this.isActionInProgress.set(false);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 | 
				
			||||||
import { CommonModule } from '@angular/common';
 | 
					import { CommonModule } from '@angular/common';
 | 
				
			||||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
 | 
					import { PlayingCardComponent } from '../playing-card/playing-card.component';
 | 
				
			||||||
import { Card } from '../../models/blackjack.model';
 | 
					import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
| 
						 | 
					@ -13,11 +13,12 @@ import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
      <div class="card p-6 !bg-accent-red">
 | 
					      <div class="card p-6 !bg-accent-red">
 | 
				
			||||||
        <div class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-red-400 rounded-lg">
 | 
					        <div class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-red-400 rounded-lg">
 | 
				
			||||||
          @if (cards.length > 0) {
 | 
					          @if (cards.length > 0) {
 | 
				
			||||||
            @for (card of cards; track card) {
 | 
					            @for (card of cardsWithState; track card.id) {
 | 
				
			||||||
              <app-playing-card
 | 
					              <app-playing-card
 | 
				
			||||||
                [rank]="card.rank"
 | 
					                [rank]="card.rank"
 | 
				
			||||||
                [suit]="card.suit"
 | 
					                [suit]="card.suit"
 | 
				
			||||||
                [hidden]="card.hidden"
 | 
					                [hidden]="card.hidden"
 | 
				
			||||||
 | 
					                [isNew]="card.isNew"
 | 
				
			||||||
              ></app-playing-card>
 | 
					              ></app-playing-card>
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          } @else {
 | 
					          } @else {
 | 
				
			||||||
| 
						 | 
					@ -31,6 +32,33 @@ import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
  `,
 | 
					  `,
 | 
				
			||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class DealerHandComponent {
 | 
					export class DealerHandComponent implements OnChanges {
 | 
				
			||||||
  @Input() cards: Card[] = [];
 | 
					  @Input() cards: Card[] = [];
 | 
				
			||||||
 | 
					  cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  private lastCardCount = 0;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  ngOnChanges(changes: SimpleChanges): void {
 | 
				
			||||||
 | 
					    if (changes['cards']) {
 | 
				
			||||||
 | 
					      this.updateCardsWithState();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  private updateCardsWithState(): void {
 | 
				
			||||||
 | 
					    const newCards = this.cards.length > this.lastCardCount;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    this.cardsWithState = this.cards.map((card, index) => {
 | 
				
			||||||
 | 
					      // Consider a card new if it's added after the initial state and is the latest card
 | 
				
			||||||
 | 
					      const isNew = newCards && index >= this.lastCardCount;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...card,
 | 
				
			||||||
 | 
					        isNew,
 | 
				
			||||||
 | 
					        // Generate a unique ID to help Angular track the cards
 | 
				
			||||||
 | 
					        id: `${card.suit}-${card.rank}-${index}`
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    this.lastCardCount = this.cards.length;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,28 +19,48 @@ import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
      <div class="flex justify-center gap-4">
 | 
					      <div class="flex justify-center gap-4">
 | 
				
			||||||
        <button
 | 
					        <button
 | 
				
			||||||
          (click)="hit.emit()"
 | 
					          (click)="hit.emit()"
 | 
				
			||||||
          class="button-primary px-8 py-4 text-lg font-medium min-w-[120px]"
 | 
					          class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
 | 
				
			||||||
          [disabled]="gameState !== 'IN_PROGRESS'"
 | 
					          [disabled]="gameState !== 'IN_PROGRESS' || isActionInProgress"
 | 
				
			||||||
 | 
					          [class.opacity-50]="isActionInProgress"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          Ziehen
 | 
					          <span [class.invisible]="isActionInProgress">Ziehen</span>
 | 
				
			||||||
 | 
					          @if (isActionInProgress) {
 | 
				
			||||||
 | 
					            <div class="absolute inset-0 flex items-center justify-center">
 | 
				
			||||||
 | 
					              <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
        <button
 | 
					        <button
 | 
				
			||||||
          (click)="stand.emit()"
 | 
					          (click)="stand.emit()"
 | 
				
			||||||
          class="button-primary px-8 py-4 text-lg font-medium min-w-[120px]"
 | 
					          class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
 | 
				
			||||||
          [disabled]="gameState !== 'IN_PROGRESS'"
 | 
					          [disabled]="gameState !== 'IN_PROGRESS' || isActionInProgress"
 | 
				
			||||||
 | 
					          [class.opacity-50]="isActionInProgress"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          Halten
 | 
					          <span [class.invisible]="isActionInProgress">Halten</span>
 | 
				
			||||||
 | 
					          @if (isActionInProgress) {
 | 
				
			||||||
 | 
					            <div class="absolute inset-0 flex items-center justify-center">
 | 
				
			||||||
 | 
					              <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
        <button
 | 
					        <button
 | 
				
			||||||
          (click)="doubleDown.emit()"
 | 
					          (click)="doubleDown.emit()"
 | 
				
			||||||
          class="button-primary px-8 py-4 text-lg font-medium min-w-[120px]"
 | 
					          class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
 | 
				
			||||||
          [disabled]="gameState !== 'IN_PROGRESS' || playerCards.length !== 2"
 | 
					          [disabled]="gameState !== 'IN_PROGRESS' || playerCards.length !== 2 || isActionInProgress"
 | 
				
			||||||
 | 
					          [class.opacity-50]="isActionInProgress"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          Verdoppeln
 | 
					          <span [class.invisible]="isActionInProgress">Verdoppeln</span>
 | 
				
			||||||
 | 
					          @if (isActionInProgress) {
 | 
				
			||||||
 | 
					            <div class="absolute inset-0 flex items-center justify-center">
 | 
				
			||||||
 | 
					              <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
        <button
 | 
					        <button
 | 
				
			||||||
          (click)="leave.emit()"
 | 
					          (click)="leave.emit()"
 | 
				
			||||||
          class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
 | 
					          class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
 | 
				
			||||||
 | 
					          [disabled]="isActionInProgress"
 | 
				
			||||||
 | 
					          [class.opacity-50]="isActionInProgress"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          Abbrechen
 | 
					          Abbrechen
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
| 
						 | 
					@ -52,6 +72,7 @@ import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
export class GameControlsComponent {
 | 
					export class GameControlsComponent {
 | 
				
			||||||
  @Input() playerCards: Card[] = [];
 | 
					  @Input() playerCards: Card[] = [];
 | 
				
			||||||
  @Input() gameState: string = 'IN_PROGRESS';
 | 
					  @Input() gameState: string = 'IN_PROGRESS';
 | 
				
			||||||
 | 
					  @Input() isActionInProgress: boolean = false;
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  @Output() hit = new EventEmitter<void>();
 | 
					  @Output() hit = new EventEmitter<void>();
 | 
				
			||||||
  @Output() stand = new EventEmitter<void>();
 | 
					  @Output() stand = new EventEmitter<void>();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -86,10 +86,15 @@ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angula
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <button
 | 
					          <button
 | 
				
			||||||
            type="submit"
 | 
					            type="submit"
 | 
				
			||||||
            class="button-primary w-full py-2"
 | 
					            class="button-primary w-full py-2 relative"
 | 
				
			||||||
            [disabled]="!betForm.valid || gameInProgress"
 | 
					            [disabled]="!betForm.valid || gameInProgress || isActionInProgress"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            Neues Spiel
 | 
					            <span [class.invisible]="isActionInProgress">Neues Spiel</span>
 | 
				
			||||||
 | 
					            @if (isActionInProgress) {
 | 
				
			||||||
 | 
					              <div class="absolute inset-0 flex items-center justify-center">
 | 
				
			||||||
 | 
					                <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
        </form>
 | 
					        </form>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
| 
						 | 
					@ -101,6 +106,7 @@ export class GameInfoComponent implements OnChanges {
 | 
				
			||||||
  @Input() balance = 0;
 | 
					  @Input() balance = 0;
 | 
				
			||||||
  @Input() currentBet = 0;
 | 
					  @Input() currentBet = 0;
 | 
				
			||||||
  @Input() gameInProgress = false;
 | 
					  @Input() gameInProgress = false;
 | 
				
			||||||
 | 
					  @Input() isActionInProgress = false;
 | 
				
			||||||
  @Output() newGame = new EventEmitter<number>();
 | 
					  @Output() newGame = new EventEmitter<number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  betForm: FormGroup;
 | 
					  betForm: FormGroup;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 | 
				
			||||||
import { CommonModule } from '@angular/common';
 | 
					import { CommonModule } from '@angular/common';
 | 
				
			||||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
 | 
					import { PlayingCardComponent } from '../playing-card/playing-card.component';
 | 
				
			||||||
import { Card } from '../../models/blackjack.model';
 | 
					import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
| 
						 | 
					@ -15,11 +15,12 @@ import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
          class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg"
 | 
					          class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          @if (cards.length > 0) {
 | 
					          @if (cards.length > 0) {
 | 
				
			||||||
            @for (card of cards; track card) {
 | 
					            @for (card of cardsWithState; track card.id) {
 | 
				
			||||||
              <app-playing-card
 | 
					              <app-playing-card
 | 
				
			||||||
                [rank]="card.rank"
 | 
					                [rank]="card.rank"
 | 
				
			||||||
                [suit]="card.suit"
 | 
					                [suit]="card.suit"
 | 
				
			||||||
                [hidden]="card.hidden"
 | 
					                [hidden]="card.hidden"
 | 
				
			||||||
 | 
					                [isNew]="card.isNew"
 | 
				
			||||||
              ></app-playing-card>
 | 
					              ></app-playing-card>
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          } @else {
 | 
					          } @else {
 | 
				
			||||||
| 
						 | 
					@ -33,6 +34,33 @@ import { Card } from '../../models/blackjack.model';
 | 
				
			||||||
  `,
 | 
					  `,
 | 
				
			||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class PlayerHandComponent {
 | 
					export class PlayerHandComponent implements OnChanges {
 | 
				
			||||||
  @Input() cards: Card[] = [];
 | 
					  @Input() cards: Card[] = [];
 | 
				
			||||||
 | 
					  cardsWithState: (Card & { isNew: boolean; id: string })[] = [];
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  private lastCardCount = 0;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  ngOnChanges(changes: SimpleChanges): void {
 | 
				
			||||||
 | 
					    if (changes['cards']) {
 | 
				
			||||||
 | 
					      this.updateCardsWithState();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  private updateCardsWithState(): void {
 | 
				
			||||||
 | 
					    const newCards = this.cards.length > this.lastCardCount;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    this.cardsWithState = this.cards.map((card, index) => {
 | 
				
			||||||
 | 
					      // Consider a card new if it's added after the initial state and is the latest card
 | 
				
			||||||
 | 
					      const isNew = newCards && index >= this.lastCardCount;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...card,
 | 
				
			||||||
 | 
					        isNew,
 | 
				
			||||||
 | 
					        // Generate a unique ID to help Angular track the cards
 | 
				
			||||||
 | 
					        id: `${card.suit}-${card.rank}-${index}`
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    this.lastCardCount = this.cards.length;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, Input, AfterViewInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core';
 | 
				
			||||||
import { CommonModule } from '@angular/common';
 | 
					import { CommonModule } from '@angular/common';
 | 
				
			||||||
import { suitSymbols, Suit } from '../../models/blackjack.model';
 | 
					import { suitSymbols, Suit } from '../../models/blackjack.model';
 | 
				
			||||||
 | 
					import { gsap } from 'gsap';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-playing-card',
 | 
					  selector: 'app-playing-card',
 | 
				
			||||||
| 
						 | 
					@ -8,31 +9,90 @@ import { suitSymbols, Suit } from '../../models/blackjack.model';
 | 
				
			||||||
  imports: [CommonModule],
 | 
					  imports: [CommonModule],
 | 
				
			||||||
  template: `
 | 
					  template: `
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      class="w-24 h-36 rounded-lg p-2 relative flex flex-col justify-between shadow-lg"
 | 
					      #cardElement
 | 
				
			||||||
 | 
					      class="w-24 h-36 rounded-lg p-2 relative flex flex-col justify-between shadow-lg card-element"
 | 
				
			||||||
      [class]="hidden ? 'bg-red-800' : 'bg-white'"
 | 
					      [class]="hidden ? 'bg-red-800' : 'bg-white'"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      @if (!hidden) {
 | 
					      @if (!hidden) {
 | 
				
			||||||
        <span class="text-xl font-bold text-accent-red">{{ getDisplayRank(rank) }}</span>
 | 
					        <span class="text-xl font-bold" [class]="isRedSuit ? 'text-accent-red' : 'text-black'">{{ getDisplayRank(rank) }}</span>
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      @if (!hidden) {
 | 
					      @if (!hidden) {
 | 
				
			||||||
        <span
 | 
					        <span
 | 
				
			||||||
          class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-3xl text-accent-red"
 | 
					          class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-3xl"
 | 
				
			||||||
 | 
					          [class]="isRedSuit ? 'text-accent-red' : 'text-black'"
 | 
				
			||||||
          >{{ getSuitSymbol(suit) }}</span
 | 
					          >{{ getSuitSymbol(suit) }}</span
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      @if (!hidden) {
 | 
					      @if (!hidden) {
 | 
				
			||||||
        <span class="text-xl font-bold text-accent-red self-end rotate-180">{{
 | 
					        <span class="text-xl font-bold self-end rotate-180" [class]="isRedSuit ? 'text-accent-red' : 'text-black'">{{
 | 
				
			||||||
          getDisplayRank(rank)
 | 
					          getDisplayRank(rank)
 | 
				
			||||||
        }}</span>
 | 
					        }}</span>
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  `,
 | 
					  `,
 | 
				
			||||||
 | 
					  styles: [`
 | 
				
			||||||
 | 
					    .card-element {
 | 
				
			||||||
 | 
					      transform-style: preserve-3d;
 | 
				
			||||||
 | 
					      backface-visibility: hidden;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  `],
 | 
				
			||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class PlayingCardComponent {
 | 
					export class PlayingCardComponent implements AfterViewInit, OnChanges {
 | 
				
			||||||
  @Input({ required: true }) rank!: string;
 | 
					  @Input({ required: true }) rank!: string;
 | 
				
			||||||
  @Input({ required: true }) suit!: Suit;
 | 
					  @Input({ required: true }) suit!: Suit;
 | 
				
			||||||
  @Input({ required: true }) hidden!: boolean;
 | 
					  @Input({ required: true }) hidden!: boolean;
 | 
				
			||||||
 | 
					  @Input() isNew: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(private elementRef: ElementRef) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get isRedSuit(): boolean {
 | 
				
			||||||
 | 
					    return this.suit === 'HEARTS' || this.suit === 'DIAMONDS';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngAfterViewInit(): void {
 | 
				
			||||||
 | 
					    if (this.isNew) {
 | 
				
			||||||
 | 
					      this.animateNewCard();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnChanges(changes: SimpleChanges): void {
 | 
				
			||||||
 | 
					    if (changes['hidden'] && !changes['hidden'].firstChange) {
 | 
				
			||||||
 | 
					      this.animateCardFlip();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private animateNewCard(): void {
 | 
				
			||||||
 | 
					    const cardElement = this.elementRef.nativeElement.querySelector('.card-element');
 | 
				
			||||||
 | 
					    gsap.fromTo(
 | 
				
			||||||
 | 
					      cardElement,
 | 
				
			||||||
 | 
					      { 
 | 
				
			||||||
 | 
					        y: -100, 
 | 
				
			||||||
 | 
					        opacity: 0,
 | 
				
			||||||
 | 
					        rotation: -10,
 | 
				
			||||||
 | 
					        scale: 0.7
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { 
 | 
				
			||||||
 | 
					        y: 0, 
 | 
				
			||||||
 | 
					        opacity: 1,
 | 
				
			||||||
 | 
					        rotation: 0,
 | 
				
			||||||
 | 
					        scale: 1,
 | 
				
			||||||
 | 
					        duration: 0.5, 
 | 
				
			||||||
 | 
					        ease: 'power2.out'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private animateCardFlip(): void {
 | 
				
			||||||
 | 
					    const cardElement = this.elementRef.nativeElement.querySelector('.card-element');
 | 
				
			||||||
 | 
					    gsap.to(cardElement, { 
 | 
				
			||||||
 | 
					      rotationY: 180, 
 | 
				
			||||||
 | 
					      duration: 0.3,
 | 
				
			||||||
 | 
					      onComplete: () => {
 | 
				
			||||||
 | 
					        gsap.set(cardElement, { rotationY: 0 });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected getSuitSymbol(suit: Suit): string {
 | 
					  protected getSuitSymbol(suit: Suit): string {
 | 
				
			||||||
    return suitSymbols[suit];
 | 
					    return suitSymbols[suit];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Reference in a new issue