feat(slots): add slot machine component with styling and logic
All checks were successful
CI / Get Changed Files (pull_request) Successful in 9s
CI / Checkstyle Main (pull_request) Has been skipped
CI / Docker backend validation (pull_request) Successful in 9s
CI / oxlint (pull_request) Successful in 24s
CI / Docker frontend validation (pull_request) Successful in 39s
CI / eslint (pull_request) Successful in 30s
CI / prettier (pull_request) Successful in 29s
CI / test-build (pull_request) Successful in 1m0s

This commit is contained in:
Jan-Marlon Leibl 2025-05-07 16:25:43 +02:00
commit fd079d2460
Signed by: jleibl
GPG key ID: 300B2F906DC6F1D5
3 changed files with 282 additions and 48 deletions

View file

@ -0,0 +1,28 @@
/* Open button styling - Matches lootbox component style */
.open-btn {
background: linear-gradient(90deg, #4338ca 0%, #8b5cf6 100%);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
}
.open-btn:hover {
background: linear-gradient(90deg, #4f46e5 0%, #a78bfa 100%);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.3);
}
/* Symbol colors */
.symbol-BAR {
color: var(--color-accent-yellow);
}
.symbol-SEVEN {
color: var(--color-accent-red);
}
.symbol-BELL {
color: var(--color-accent-purple);
}
.symbol-CHERRY {
color: #ec4899;
}
.symbol-LEMON {
color: #a3e635;
}

View file

@ -1,40 +1,66 @@
<app-navbar></app-navbar>
<div>
<h2>Payouts</h2>
@if (slotInfo(); as info) {
<table>
<tbody>
@for (item of info | keyvalue; track item.key) {
<tr>
<td>{{ item.key }}</td>
<td>{{ item.value }}</td>
</tr>
}
</tbody>
</table>
}
<div class="container mx-auto px-4 py-6 space-y-8">
<h1 class="text-3xl font-bold text-white mb-6">Spielautomaten</h1>
<div>
<div class="grid grid-cols-3 gap-1">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- Slot Machine Display -->
<div class="lg:col-span-3 space-y-6 flex flex-col">
<div class="card">
<!-- Slot Machine Top -->
<div class="p-6">
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-white">Slot Machine</h2>
<div
[ngClass]="{
'bg-emerald': slotResult().status === 'win',
'bg-accent-red': slotResult().status === 'lose',
'bg-deep-blue-light': slotResult().status === 'start',
}"
class="px-3 py-1 rounded-full text-sm font-semibold"
>
<span class="text-white">
{{
slotResult().status === 'win'
? 'Gewonnen!'
: slotResult().status === 'lose'
? 'Verloren'
: 'Bereit'
}}
</span>
</div>
</div>
</div>
<!-- Slot Display -->
<div class="p-6">
<div class="bg-deep-blue-light rounded-lg p-4 shadow-inner mb-6">
<div class="grid grid-cols-3 gap-3">
@for (row of slotResult().resultMatrix; track $index) {
@for (cell of row; track $index) {
<div class="text-center">{{ cell }}</div>
<div
class="bg-deep-blue-contrast rounded-lg shadow-md p-2 flex items-center justify-center"
>
<span class="text-2xl font-bold" [ngClass]="getSymbolClass(cell)">{{
cell
}}</span>
</div>
}
}
</div>
<div>
<p>
Game result: <strong>{{ slotResult().status | uppercase }}</strong>
</p>
<p>
Amount: <strong>{{ slotResult().amount }}</strong>
</p>
</div>
<div>
<label for="betAmount">Bet Amount: </label>
<!-- Game Result -->
<div class="text-center mb-6" *ngIf="slotResult().status === 'win'">
<div class="text-emerald text-xl font-bold">
+{{ slotResult().amount | currency: 'EUR' }}
</div>
</div>
<!-- Controls -->
<div class="flex flex-col sm:flex-row gap-4 items-center justify-center">
<div class="flex items-center bg-deep-blue-light rounded-lg p-2 flex-1">
<label for="betAmount" class="text-text-secondary mr-3">Einsatz:</label>
<input
id="betAmount"
type="number"
@ -42,9 +68,111 @@
(ngModelChange)="betAmount.set($event)"
step="0.01"
min="0.01"
class="w-full bg-deep-blue-light text-white focus:outline-none focus:ring-1 focus:ring-emerald"
/>
</div>
<button (click)="spin()">SPIN</button>
<button
(click)="spin()"
class="px-4 py-1.5 font-bold rounded"
[ngClass]="{
'open-btn': hasEnoughBalance(),
'bg-gray-500 cursor-not-allowed': !hasEnoughBalance(),
}"
[disabled]="isSpinning || !hasEnoughBalance()"
>
@if (!isSpinning) {
<span>{{ hasEnoughBalance() ? 'SPIN' : 'Nicht genug Guthaben' }}</span>
} @else {
<div
class="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin"
></div>
}
</button>
</div>
</div>
</div>
</div>
<!-- Game Info Panel -->
<div class="lg:col-span-1">
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-text-secondary">Kontostand:</span>
<span class="text-emerald">
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-text-secondary">Einsatz:</span>
<span [class]="betAmount() > 0 ? 'text-accent-red' : 'text-text-secondary'">
<app-animated-number [value]="betAmount()" [duration]="0.5"></app-animated-number>
</span>
</div>
<div class="grid grid-cols-2 gap-2 mb-4">
<button
(click)="setBetAmount(0.1)"
class="button-primary py-2 text-sm"
[disabled]="isSpinning"
>
10%
</button>
<button
(click)="setBetAmount(0.25)"
class="button-primary py-2 text-sm"
[disabled]="isSpinning"
>
25%
</button>
<button
(click)="setBetAmount(0.5)"
class="button-primary py-2 text-sm"
[disabled]="isSpinning"
>
50%
</button>
<button
(click)="setBetAmount(1)"
class="button-primary py-2 text-sm"
[disabled]="isSpinning"
>
100%
</button>
</div>
<h3 class="text-lg font-semibold text-white mb-2">Auszahlungen:</h3>
@if (slotInfo(); as info) {
<ul class="space-y-1">
@for (item of info | keyvalue; track item.key) {
<li class="flex justify-between items-center py-1 border-b border-deep-blue-light">
<div class="bg-deep-blue-contrast px-2 py-1 rounded text-center w-12">
<span [ngClass]="getSymbolClass(item.key)">{{ item.key }}</span>
</div>
<span class="text-emerald">{{ item.value }}x</span>
</li>
}
</ul>
} @else {
<div class="flex justify-center py-4">
<div
class="w-4 h-4 border-2 border-deep-blue-contrast border-t-emerald rounded-full animate-spin"
></div>
</div>
}
<div class="mt-4 pt-2">
<h4 class="text-sm font-semibold text-white mb-2">Spielregeln:</h4>
<ul class="text-text-secondary text-xs space-y-1">
<li>• Gewinne mit 3 gleichen Symbolen</li>
<li>• Höhere Symbole = höhere Gewinne</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,8 +1,18 @@
import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
inject,
OnInit,
OnDestroy,
signal,
} from '@angular/core';
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
import { HttpClient } from '@angular/common/http';
import { KeyValuePipe, NgClass, UpperCasePipe } from '@angular/common';
import { CommonModule, KeyValuePipe, NgClass, CurrencyPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { UserService } from '@service/user.service';
import { Subscription } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
interface SlotResult {
status: 'win' | 'lose' | 'blank' | 'start';
@ -13,12 +23,24 @@ interface SlotResult {
@Component({
selector: 'app-slots',
standalone: true,
imports: [NavbarComponent, KeyValuePipe, UpperCasePipe, NgClass, FormsModule],
imports: [
CommonModule,
NavbarComponent,
KeyValuePipe,
NgClass,
FormsModule,
CurrencyPipe,
AnimatedNumberComponent,
],
templateUrl: './slots.component.html',
styleUrl: './slots.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class SlotsComponent implements OnInit {
export default class SlotsComponent implements OnInit, OnDestroy {
private httpClient: HttpClient = inject(HttpClient);
private userService = inject(UserService);
private userSubscription: Subscription | undefined;
slotInfo = signal<Record<string, number> | null>(null);
slotResult = signal<SlotResult>({
status: 'start',
@ -29,21 +51,77 @@ export default class SlotsComponent implements OnInit {
['BELL', 'BELL', 'BELL'],
],
});
balance = signal<number>(0);
betAmount = signal<number>(1);
isSpinning = false;
ngOnInit(): void {
this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => {
this.slotInfo.set(data);
});
this.userSubscription = this.userService.currentUser$.subscribe((user) => {
this.balance.set(user?.balance ?? 0);
});
this.userService.refreshCurrentUser();
}
ngOnDestroy(): void {
if (this.userSubscription) {
this.userSubscription.unsubscribe();
}
}
getSymbolClass(symbol: string): string {
return `symbol-${symbol}`;
}
hasEnoughBalance(): boolean {
return this.balance() >= this.betAmount();
}
setBetAmount(percentage: number): void {
const calculatedBet = Math.floor(this.balance() * percentage * 100) / 100;
const minimumBet = 0.01;
const newBet = Math.max(minimumBet, Math.min(calculatedBet, this.balance()));
this.betAmount.set(newBet);
}
spin(): void {
if (!this.hasEnoughBalance()) {
return;
}
this.isSpinning = true;
const betAmount = this.betAmount();
this.userService.updateLocalBalance(-betAmount);
const payload = {
betAmount: this.betAmount(),
betAmount: betAmount,
};
this.httpClient.post<SlotResult>('/backend/slots/spin', payload).subscribe((result) => {
this.httpClient.post<SlotResult>('/backend/slots/spin', payload).subscribe({
next: (result) => {
setTimeout(() => {
this.slotResult.set(result);
if (result.status === 'win') {
this.userService.updateLocalBalance(result.amount);
}
this.isSpinning = false;
}, 1500);
},
error: (err) => {
console.error('Error spinning slot machine:', err);
this.userService.updateLocalBalance(betAmount);
this.isSpinning = false;
},
});
}
}