diff --git a/frontend/bun.lock b/frontend/bun.lock index 7112f3e..e51c957 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -21,6 +21,7 @@ "@tailwindcss/postcss": "^4.0.3", "ajv": "8.17.1", "ajv-formats": "3.0.1", + "countup.js": "^2.8.0", "gsap": "^3.12.7", "keycloak-angular": "^19.0.0", "keycloak-js": "^26.0.0", @@ -897,6 +898,13 @@ "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], +<<<<<<< HEAD +======= + "countup.js": ["countup.js@2.8.0", "", {}, "sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ=="], + + "critters": ["critters@0.0.24", "", { "dependencies": { "chalk": "^4.1.0", "css-select": "^5.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.2", "htmlparser2": "^8.0.2", "postcss": "^8.4.23", "postcss-media-query-parser": "^0.2.3" } }, "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q=="], + +>>>>>>> f2d447a (feat(blackjack): add animated number component and usage) "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "css-loader": ["css-loader@7.1.2", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.5.4" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA=="], diff --git a/frontend/package.json b/frontend/package.json index 4775e66..2ee7ed0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "@tailwindcss/postcss": "^4.0.3", "ajv": "8.17.1", "ajv-formats": "3.0.1", + "countup.js": "^2.8.0", "gsap": "^3.12.7", "keycloak-angular": "^19.0.0", "keycloak-js": "^26.0.0", diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.html b/frontend/src/app/feature/game/blackjack/blackjack.component.html index 93328a3..aa7426a 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.html +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.html @@ -6,19 +6,6 @@ <app-dealer-hand [cards]="dealerCards()"></app-dealer-hand> <app-player-hand [cards]="playerCards()"></app-player-hand> - @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()) { <app-game-controls [playerCards]="playerCards()" diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.ts b/frontend/src/app/feature/game/blackjack/blackjack.component.ts index 702776e..370b535 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.ts +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.ts @@ -48,7 +48,6 @@ export default class BlackjackComponent implements OnInit { showGameResult = signal(false); isActionInProgress = signal(false); - currentAction = signal<string>(''); showDebtDialog = signal(false); debtAmount = signal(0); @@ -96,7 +95,6 @@ export default class BlackjackComponent implements OnInit { onNewGame(bet: number): void { this.isActionInProgress.set(true); - this.currentAction.set('Spiel wird gestartet...'); this.blackjackService.startGame(bet).subscribe({ next: (game) => { @@ -115,7 +113,6 @@ export default class BlackjackComponent implements OnInit { if (!this.currentGameId() || this.isActionInProgress()) return; this.isActionInProgress.set(true); - this.currentAction.set('Karte wird gezogen...'); this.blackjackService.hit(this.currentGameId()!).subscribe({ next: (game) => { @@ -142,7 +139,6 @@ export default class BlackjackComponent implements OnInit { } this.isActionInProgress.set(true); - this.currentAction.set('Dealer zieht Karten...'); this.blackjackService.stand(this.currentGameId()!).subscribe({ next: (game) => { @@ -167,7 +163,6 @@ export default class BlackjackComponent implements OnInit { } this.isActionInProgress.set(true); - this.currentAction.set('Einsatz wird verdoppelt...'); this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ next: (game) => { diff --git a/frontend/src/app/feature/game/blackjack/components/animated-number/animated-number.component.ts b/frontend/src/app/feature/game/blackjack/components/animated-number/animated-number.component.ts new file mode 100644 index 0000000..aa4c1b6 --- /dev/null +++ b/frontend/src/app/feature/game/blackjack/components/animated-number/animated-number.component.ts @@ -0,0 +1,80 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; +import { CommonModule, CurrencyPipe } from '@angular/common'; +import { CountUp } from 'countup.js'; + +@Component({ + selector: 'app-animated-number', + standalone: true, + imports: [CommonModule, CurrencyPipe], + template: ` + <span #numberElement>{{ formattedValue }}</span> + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AnimatedNumberComponent implements OnChanges, AfterViewInit { + @Input() value = 0; + @Input() duration = 1; + @Input() ease = 'power1.out'; + + @ViewChild('numberElement') numberElement!: ElementRef; + + private countUp: CountUp | null = null; + private previousValue = 0; + formattedValue = '0,00 €'; + + ngAfterViewInit(): void { + this.initializeCountUp(); + if (this.countUp && this.value !== 0) { + this.countUp.start(() => { + this.previousValue = this.value; + }); + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['value']) { + if (this.countUp) { + const startVal = this.previousValue; + const endVal = this.value; + + // Update the CountUp instance with new start and end values + this.countUp.update(endVal); + this.previousValue = endVal; + } else { + // Format the initial value if CountUp is not yet initialized + this.formattedValue = new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(this.value); + } + } + } + + private initializeCountUp(): void { + if (this.numberElement) { + this.countUp = new CountUp(this.numberElement.nativeElement, this.value, { + startVal: this.previousValue, + duration: this.duration, + easingFn: (t, b, c, d) => { + // Custom easing function based on the input ease type + if (this.ease === 'power1.out') { + return c * (1 - Math.pow(1 - t / d, 1)) + b; + } + return c * (t / d) + b; // linear fallback + }, + formattingFn: (value) => { + const formatted = new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); + this.formattedValue = formatted; + return formatted; + }, + }); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts b/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts index d01adc6..9799b08 100644 --- a/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts @@ -28,54 +28,27 @@ import { GameControlsService } from '@blackjack/services/game-controls.service'; (click)="hit.emit()" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative" [disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress" - [class.opacity-50]="isActionInProgress" > - <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> - } + <span>Ziehen</span> </button> <button (click)="stand.emit()" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative" [disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress" - [class.opacity-50]="isActionInProgress" > - <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> - } + <span>Halten</span> </button> <button (click)="doubleDown.emit()" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative" - [disabled]=" - gameState !== GameState.IN_PROGRESS || playerCards.length !== 2 || isActionInProgress - " - [class.opacity-50]="isActionInProgress" + [disabled]="!canDoubleDown || isActionInProgress" > - <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> - } + <span>Verdoppeln</span> </button> <button (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" [disabled]="isActionInProgress" - [class.opacity-50]="isActionInProgress" > Abbrechen </button> @@ -97,4 +70,12 @@ export class GameControlsComponent { protected readonly GameState = GameState; constructor(protected gameControlsService: GameControlsService) {} + + get canDoubleDown(): boolean { + return ( + this.gameState === GameState.IN_PROGRESS && + this.playerCards.length === 2 && + !this.isActionInProgress + ); + } } diff --git a/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts b/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts index df29e81..ecd1fad 100644 --- a/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts @@ -11,11 +11,12 @@ import { import { CommonModule, CurrencyPipe } from '@angular/common'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { BettingService } from '@blackjack/services/betting.service'; +import { AnimatedNumberComponent } from '../animated-number/animated-number.component'; @Component({ selector: 'app-game-info', standalone: true, - imports: [CommonModule, CurrencyPipe, ReactiveFormsModule], + imports: [CommonModule, CurrencyPipe, ReactiveFormsModule, AnimatedNumberComponent], template: ` <div class="card p-4"> <h3 class="section-heading text-xl mb-4">Spiel Informationen</h3> @@ -23,7 +24,7 @@ import { BettingService } from '@blackjack/services/betting.service'; <div class="flex justify-between items-center"> <span class="text-text-secondary">Aktuelle Wette:</span> <span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'"> - {{ currentBet | currency: 'EUR' }} + <app-animated-number [value]="currentBet" [duration]="0.5"></app-animated-number> </span> </div> diff --git a/frontend/src/app/feature/game/blackjack/components/game-result/game-result.component.ts b/frontend/src/app/feature/game/blackjack/components/game-result/game-result.component.ts index 63ca955..5547e27 100644 --- a/frontend/src/app/feature/game/blackjack/components/game-result/game-result.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/game-result/game-result.component.ts @@ -2,11 +2,12 @@ import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from import { CommonModule, CurrencyPipe } from '@angular/common'; import { animate, style, transition, trigger } from '@angular/animations'; import { GameState } from '../../enum/gameState'; +import { AnimatedNumberComponent } from '../animated-number/animated-number.component'; @Component({ selector: 'app-game-result', standalone: true, - imports: [CommonModule, CurrencyPipe], + imports: [CommonModule, CurrencyPipe, AnimatedNumberComponent], template: ` <div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;"> <div class="modal-card" [@cardAnimation]> @@ -18,7 +19,9 @@ import { GameState } from '../../enum/gameState'; > <div class="grid grid-cols-2 gap-4"> <div class="text-text-secondary">Einsatz:</div> - <div class="font-medium text-right">{{ amount | currency: 'EUR' }}</div> + <div class="font-medium text-right"> + <app-animated-number [value]="amount" [duration]="0.5"></app-animated-number> + </div> <div class="text-text-secondary"> {{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }} @@ -31,9 +34,13 @@ import { GameState } from '../../enum/gameState'; 'text-yellow-400': isDraw, }" > - {{ isLoss ? '-' : '+' }}{{ isWin ? amount * 2 : (amount | currency: 'EUR') }} + {{ isLoss ? '-' : '+' }} + <app-animated-number + [value]="isWin ? amount * 2 : amount" + [duration]="0.5" + ></app-animated-number> <div *ngIf="isWin" class="text-xs text-text-secondary"> - (Einsatz {{ amount | currency: 'EUR' }} × 2) + (Einsatz <app-animated-number [value]="amount" [duration]="0.5"></app-animated-number> × 2) </div> </div> @@ -41,7 +48,7 @@ import { GameState } from '../../enum/gameState'; Kontostand: </div> <div class="font-medium text-right border-t border-text-secondary/20 pt-3"> - {{ balance | currency: 'EUR' }} + <app-animated-number [value]="balance" [duration]="0.5"></app-animated-number> </div> </div> </div> diff --git a/frontend/src/app/shared/components/debt-dialog/debt-dialog.component.ts b/frontend/src/app/shared/components/debt-dialog/debt-dialog.component.ts index dd60fdc..a97f431 100644 --- a/frontend/src/app/shared/components/debt-dialog/debt-dialog.component.ts +++ b/frontend/src/app/shared/components/debt-dialog/debt-dialog.component.ts @@ -11,18 +11,19 @@ import { import { CommonModule } from '@angular/common'; import { animate, style, transition, trigger } from '@angular/animations'; import { interval, Subscription, takeWhile } from 'rxjs'; +import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component'; @Component({ selector: 'app-debt-dialog', standalone: true, - imports: [CommonModule], + imports: [CommonModule, AnimatedNumberComponent], template: ` <div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;"> <div class="modal-card" [@cardAnimation]> <h2 class="modal-heading text-accent-red">WARNUNG!</h2> <p class="py-2 text-text-secondary mb-4"> Du hast nicht genug Geld für den Double Down. Du bist jetzt im Minus und schuldest uns - {{ amount | currency: 'EUR' }}. + <app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>. </p> <p class="py-2 text-accent-red mb-4 font-bold"> Liefer das Geld sofort an den Dead Drop oder es wird unangenehme Konsequenzen geben! @@ -32,7 +33,9 @@ import { interval, Subscription, takeWhile } from 'rxjs'; > <div class="grid grid-cols-2 gap-4"> <div class="text-text-secondary">Schulden:</div> - <div class="font-medium text-right text-accent-red">{{ amount | currency: 'EUR' }}</div> + <div class="font-medium text-right text-accent-red"> + <app-animated-number [value]="amount" [duration]="0.5"></app-animated-number> + </div> </div> </div> <div class="text-center mb-6"> diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html index b34aea5..e9f8e91 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.html +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -19,9 +19,9 @@ class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300" routerLink="/home" > - <span [class]="balance() < 0 ? 'text-accent-red' : ''">{{ - balance() | currency: 'EUR' : 'symbol' : '1.2-2' - }}</span> + <span [class]="balance() < 0 ? 'text-accent-red' : ''"> + <app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number> + </span> </div> <button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button> } diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts index 6b972ac..f1fffba 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.ts +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -11,12 +11,13 @@ import { KeycloakService } from 'keycloak-angular'; import { CurrencyPipe } from '@angular/common'; import { UserService } from '@service/user.service'; import { Subscription } from 'rxjs'; +import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', standalone: true, - imports: [RouterModule, CurrencyPipe], + imports: [RouterModule, CurrencyPipe, AnimatedNumberComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) export class NavbarComponent implements OnInit, OnDestroy {