Compare commits

...

2 commits

Author SHA1 Message Date
30b4cd4d8d
refactor: remove unnecessary comments and variables
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Successful in 18s
CI / test-build (pull_request) Successful in 27s
CI / prettier (pull_request) Failing after 34s
2025-04-03 10:03:37 +02:00
f2d447abeb
feat(blackjack): add animated number component and usage
Some checks failed
CI / Get Changed Files (pull_request) Successful in 27s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Failing after 41s
CI / test-build (pull_request) Successful in 49s
CI / prettier (pull_request) Failing after 58s
2025-04-03 10:02:15 +02:00
12 changed files with 118 additions and 64 deletions

View file

@ -19,6 +19,7 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@stripe/stripe-js": "^5.6.0", "@stripe/stripe-js": "^5.6.0",
"@tailwindcss/postcss": "^4.0.3", "@tailwindcss/postcss": "^4.0.3",
"countup.js": "^2.8.0",
"gsap": "^3.12.7", "gsap": "^3.12.7",
"keycloak-angular": "^16.0.1", "keycloak-angular": "^16.0.1",
"keycloak-js": "^25.0.5", "keycloak-js": "^25.0.5",
@ -853,6 +854,8 @@
"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=="], "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=="],
"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=="], "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=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],

View file

@ -28,6 +28,7 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@stripe/stripe-js": "^5.6.0", "@stripe/stripe-js": "^5.6.0",
"@tailwindcss/postcss": "^4.0.3", "@tailwindcss/postcss": "^4.0.3",
"countup.js": "^2.8.0",
"gsap": "^3.12.7", "gsap": "^3.12.7",
"keycloak-angular": "^16.0.1", "keycloak-angular": "^16.0.1",
"keycloak-js": "^25.0.5", "keycloak-js": "^25.0.5",

View file

@ -6,19 +6,6 @@
<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>
@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()"

View file

@ -48,7 +48,6 @@ export default class BlackjackComponent implements OnInit {
showGameResult = signal(false); showGameResult = signal(false);
isActionInProgress = signal(false); isActionInProgress = signal(false);
currentAction = signal<string>('');
showDebtDialog = signal(false); showDebtDialog = signal(false);
debtAmount = signal(0); debtAmount = signal(0);
@ -96,7 +95,6 @@ export default class BlackjackComponent implements OnInit {
onNewGame(bet: number): void { onNewGame(bet: number): void {
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.currentAction.set('Spiel wird gestartet...');
this.blackjackService.startGame(bet).subscribe({ this.blackjackService.startGame(bet).subscribe({
next: (game) => { next: (game) => {
@ -115,7 +113,6 @@ export default class BlackjackComponent implements OnInit {
if (!this.currentGameId() || this.isActionInProgress()) return; if (!this.currentGameId() || this.isActionInProgress()) return;
this.isActionInProgress.set(true); 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) => {
@ -142,7 +139,6 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); 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) => {
@ -167,7 +163,6 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); 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) => {

View file

@ -0,0 +1,76 @@
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 endVal = this.value;
this.countUp.update(endVal);
this.previousValue = endVal;
} else {
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) => {
if (this.ease === 'power1.out') {
return c * (1 - Math.pow(1 - t / d, 1)) + b;
}
return c * (t / d) + b;
},
formattingFn: (value) => {
const formatted = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
this.formattedValue = formatted;
return formatted;
},
});
}
}
}

View file

@ -28,54 +28,27 @@ import { GameControlsService } from '@blackjack/services/game-controls.service';
(click)="hit.emit()" (click)="hit.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress" [disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
[class.opacity-50]="isActionInProgress"
> >
<span [class.invisible]="isActionInProgress">Ziehen</span> <span>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] relative" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress" [disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
[class.opacity-50]="isActionInProgress"
> >
<span [class.invisible]="isActionInProgress">Halten</span> <span>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] relative" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]=" [disabled]="!canDoubleDown || isActionInProgress"
gameState !== GameState.IN_PROGRESS || playerCards.length !== 2 || isActionInProgress
"
[class.opacity-50]="isActionInProgress"
> >
<span [class.invisible]="isActionInProgress">Verdoppeln</span> <span>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" [disabled]="isActionInProgress"
[class.opacity-50]="isActionInProgress"
> >
Abbrechen Abbrechen
</button> </button>
@ -97,4 +70,12 @@ export class GameControlsComponent {
protected readonly GameState = GameState; protected readonly GameState = GameState;
constructor(protected gameControlsService: GameControlsService) {} constructor(protected gameControlsService: GameControlsService) {}
get canDoubleDown(): boolean {
return (
this.gameState === GameState.IN_PROGRESS &&
this.playerCards.length === 2 &&
!this.isActionInProgress
);
}
} }

View file

@ -11,11 +11,12 @@ import {
import { CommonModule, CurrencyPipe } from '@angular/common'; import { CommonModule, CurrencyPipe } from '@angular/common';
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { BettingService } from '@blackjack/services/betting.service'; import { BettingService } from '@blackjack/services/betting.service';
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-game-info', selector: 'app-game-info',
standalone: true, standalone: true,
imports: [CommonModule, CurrencyPipe, ReactiveFormsModule], imports: [CommonModule, CurrencyPipe, ReactiveFormsModule, AnimatedNumberComponent],
template: ` template: `
<div class="card p-4"> <div class="card p-4">
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3> <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"> <div class="flex justify-between items-center">
<span class="text-text-secondary">Aktuelle Wette:</span> <span class="text-text-secondary">Aktuelle Wette:</span>
<span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'"> <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> </span>
</div> </div>

View file

@ -2,11 +2,12 @@ import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from
import { CommonModule, CurrencyPipe } from '@angular/common'; import { CommonModule, CurrencyPipe } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { GameState } from '../../enum/gameState'; import { GameState } from '../../enum/gameState';
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-game-result', selector: 'app-game-result',
standalone: true, standalone: true,
imports: [CommonModule, CurrencyPipe], imports: [CommonModule, CurrencyPipe, AnimatedNumberComponent],
template: ` template: `
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;"> <div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
<div class="modal-card" [@cardAnimation]> <div class="modal-card" [@cardAnimation]>
@ -18,7 +19,9 @@ import { GameState } from '../../enum/gameState';
> >
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="text-text-secondary">Einsatz:</div> <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"> <div class="text-text-secondary">
{{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }} {{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }}
@ -31,9 +34,13 @@ import { GameState } from '../../enum/gameState';
'text-yellow-400': isDraw, '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"> <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>
</div> </div>
@ -41,7 +48,7 @@ import { GameState } from '../../enum/gameState';
Kontostand: Kontostand:
</div> </div>
<div class="font-medium text-right border-t border-text-secondary/20 pt-3"> <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> </div>
</div> </div>

View file

@ -13,7 +13,6 @@ export class UserService {
public currentUser$ = this.currentUserSubject.asObservable(); public currentUser$ = this.currentUserSubject.asObservable();
constructor() { constructor() {
// Initialize with current user data
this.getCurrentUser().subscribe(); this.getCurrentUser().subscribe();
} }

View file

@ -11,18 +11,19 @@ import {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { interval, Subscription, takeWhile } from 'rxjs'; import { interval, Subscription, takeWhile } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-debt-dialog', selector: 'app-debt-dialog',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, AnimatedNumberComponent],
template: ` template: `
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;"> <div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
<div class="modal-card" [@cardAnimation]> <div class="modal-card" [@cardAnimation]>
<h2 class="modal-heading text-accent-red">WARNUNG!</h2> <h2 class="modal-heading text-accent-red">WARNUNG!</h2>
<p class="py-2 text-text-secondary mb-4"> <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 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>
<p class="py-2 text-accent-red mb-4 font-bold"> <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! 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="grid grid-cols-2 gap-4">
<div class="text-text-secondary">Schulden:</div> <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> </div>
<div class="text-center mb-6"> <div class="text-center mb-6">

View file

@ -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" 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" routerLink="/home"
> >
<span [class]="balance() < 0 ? 'text-accent-red' : ''">{{ <span [class]="balance() < 0 ? 'text-accent-red' : ''">
balance() | currency: 'EUR' : 'symbol' : '1.2-2' <app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
}}</span> </span>
</div> </div>
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button> <button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
} }

View file

@ -11,12 +11,13 @@ import { KeycloakService } from 'keycloak-angular';
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { UserService } from '@service/user.service'; import { UserService } from '@service/user.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
standalone: true, standalone: true,
imports: [RouterModule, CurrencyPipe], imports: [RouterModule, CurrencyPipe, AnimatedNumberComponent],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NavbarComponent implements OnInit, OnDestroy { export class NavbarComponent implements OnInit, OnDestroy {