Merge pull request 'feat(slots): add slot machine component with styling and logic' (!170) from task/CAS-44/lootbox-selection into main
All checks were successful
Release / Release (push) Successful in 59s
Release / Build Frontend Image (push) Successful in 29s
Release / Build Backend Image (push) Successful in 31s

Reviewed-on: #170
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
This commit is contained in:
Jan-Marlon Leibl 2025-05-07 16:10:05 +00:00
commit ba6f72fb97
No known key found for this signature in database
GPG key ID: 944223E4D46B7412
10 changed files with 447 additions and 210 deletions

View file

@ -1,91 +1,91 @@
package de.szut.casino.lootboxes; package de.szut.casino.lootboxes;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException; import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserService; import de.szut.casino.user.UserService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@RestController @RestController
public class LootBoxController { public class LootBoxController {
private final LootBoxRepository lootBoxRepository; private final LootBoxRepository lootBoxRepository;
private final UserService userService; private final UserService userService;
private final LootBoxService lootBoxService; private final LootBoxService lootBoxService;
public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) { public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) {
this.lootBoxRepository = lootBoxRepository; this.lootBoxRepository = lootBoxRepository;
this.userService = userService; this.userService = userService;
this.lootBoxService = lootBoxService; this.lootBoxService = lootBoxService;
} }
@GetMapping("/lootboxes") @GetMapping("/lootboxes")
public List<LootBoxEntity> getAllLootBoxes() { public List<LootBoxEntity> getAllLootBoxes() {
return lootBoxRepository.findAll(); return lootBoxRepository.findAll();
} }
@PostMapping("/lootboxes/{id}") @PostMapping("/lootboxes/{id}")
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) { public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id); Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) { if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
LootBoxEntity lootBox = optionalLootBox.get(); LootBoxEntity lootBox = optionalLootBox.get();
Optional<UserEntity> optionalUser = userService.getCurrentUser(); Optional<UserEntity> optionalUser = userService.getCurrentUser();
if (optionalUser.isEmpty()) { if (optionalUser.isEmpty()) {
throw new UserNotFoundException(); throw new UserNotFoundException();
} }
UserEntity user = optionalUser.get(); UserEntity user = optionalUser.get();
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) { if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
throw new InsufficientFundsException(); throw new InsufficientFundsException();
} }
RewardEntity reward = lootBoxService.determineReward(lootBox); RewardEntity reward = lootBoxService.determineReward(lootBox);
lootBoxService.handleBalance(user, lootBox, reward); lootBoxService.handleBalance(user, lootBox, reward);
return ResponseEntity.ok(reward); return ResponseEntity.ok(reward);
} }
@PostMapping("/lootboxes") @PostMapping("/lootboxes")
public ResponseEntity<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) { public ResponseEntity<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
List<RewardEntity> rewardEntities = new ArrayList<>(); List<RewardEntity> rewardEntities = new ArrayList<>();
for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) { for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) {
rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability())); rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability()));
} }
LootBoxEntity lootBoxEntity = new LootBoxEntity( LootBoxEntity lootBoxEntity = new LootBoxEntity(
createLootBoxDto.getName(), createLootBoxDto.getName(),
createLootBoxDto.getPrice(), createLootBoxDto.getPrice(),
rewardEntities rewardEntities
); );
this.lootBoxRepository.save(lootBoxEntity); this.lootBoxRepository.save(lootBoxEntity);
return ResponseEntity.ok(lootBoxEntity); return ResponseEntity.ok(lootBoxEntity);
} }
@DeleteMapping("/lootboxes/{id}") @DeleteMapping("/lootboxes/{id}")
public ResponseEntity<Object> deleteLootbox(@PathVariable Long id) { public ResponseEntity<Object> deleteLootbox(@PathVariable Long id) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id); Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) { if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
LootBoxEntity lootBox = optionalLootBox.get(); LootBoxEntity lootBox = optionalLootBox.get();
lootBoxRepository.delete(lootBox); lootBoxRepository.delete(lootBox);
return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox")); return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox"));
} }
} }

View file

@ -1,40 +1,40 @@
package de.szut.casino.lootboxes; package de.szut.casino.lootboxes;
import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository; import de.szut.casino.user.UserRepository;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
@Service @Service
public class LootBoxService { public class LootBoxService {
private final UserRepository userRepository; private final UserRepository userRepository;
public LootBoxService(UserRepository userRepository) { public LootBoxService(UserRepository userRepository) {
this.userRepository = userRepository; this.userRepository = userRepository;
} }
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) { public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
return user.getBalance().compareTo(price) < 0; return user.getBalance().compareTo(price) < 0;
} }
public RewardEntity determineReward(LootBoxEntity lootBox) { public RewardEntity determineReward(LootBoxEntity lootBox) {
double randomValue = Math.random(); double randomValue = Math.random();
BigDecimal cumulativeProbability = BigDecimal.ZERO; BigDecimal cumulativeProbability = BigDecimal.ZERO;
for (RewardEntity reward : lootBox.getRewards()) { for (RewardEntity reward : lootBox.getRewards()) {
cumulativeProbability = cumulativeProbability.add(reward.getProbability()); cumulativeProbability = cumulativeProbability.add(reward.getProbability());
if (randomValue <= cumulativeProbability.doubleValue()) { if (randomValue <= cumulativeProbability.doubleValue()) {
return reward; return reward;
} }
} }
return lootBox.getRewards().getLast(); return lootBox.getRewards().getLast();
} }
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) { public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
user.setBalance(user.getBalance().subtract(lootBox.getPrice())); user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
user.setBalance(user.getBalance().add(reward.getValue())); user.setBalance(user.getBalance().add(reward.getValue()));
userRepository.save(user); userRepository.save(user);
} }
} }

View file

@ -54,12 +54,13 @@ public class SlotService {
SpinResult spinResult = new SpinResult(); SpinResult spinResult = new SpinResult();
spinResult.setStatus(status.name().toLowerCase()); spinResult.setStatus(status.name().toLowerCase());
this.balanceService.subtractFunds(user, betAmount);
if (status == Status.WIN) { if (status == Status.WIN) {
BigDecimal winAmount = betAmount.multiply(winSymbol.getPayoutMultiplier()); BigDecimal winAmount = betAmount.multiply(winSymbol.getPayoutMultiplier());
this.balanceService.addFunds(user, winAmount); this.balanceService.addFunds(user, winAmount);
spinResult.setAmount(winAmount); spinResult.setAmount(winAmount);
} else { } else {
this.balanceService.subtractFunds(user, betAmount);
spinResult.setAmount(betAmount); spinResult.setAmount(betAmount);
} }

View file

@ -14,6 +14,7 @@ import { NavbarComponent } from '@shared/components/navbar/navbar.component';
import { UserService } from '@service/user.service'; import { UserService } from '@service/user.service';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component'; import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
import { AuthService } from '@service/auth.service';
@Component({ @Component({
selector: 'app-blackjack', selector: 'app-blackjack',
@ -34,6 +35,7 @@ import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.
export default class BlackjackComponent implements OnInit { export default class BlackjackComponent implements OnInit {
private router = inject(Router); private router = inject(Router);
private userService = inject(UserService); private userService = inject(UserService);
private authService = inject(AuthService);
private blackjackService = inject(BlackjackService); private blackjackService = inject(BlackjackService);
dealerCards = signal<Card[]>([]); dealerCards = signal<Card[]>([]);
@ -51,7 +53,8 @@ export default class BlackjackComponent implements OnInit {
debtAmount = signal(0); debtAmount = signal(0);
ngOnInit(): void { ngOnInit(): void {
this.userService.getCurrentUser().subscribe((user) => { // Subscribe to user updates for real-time balance changes
this.authService.userSubject.subscribe((user) => {
if (user) { if (user) {
this.balance.set(user.balance); this.balance.set(user.balance);
} }
@ -84,9 +87,14 @@ export default class BlackjackComponent implements OnInit {
if (isGameOver) { if (isGameOver) {
console.log('Game is over, state:', game.state); console.log('Game is over, state:', game.state);
this.userService.refreshCurrentUser(); this.userService.refreshCurrentUser();
timer(1500).subscribe(() => {
this.showGameResult.set(true); // Get the latest balance before showing the result dialog
console.log('Game result dialog shown after delay'); timer(1000).subscribe(() => {
// Show the result dialog after refreshing user data
timer(500).subscribe(() => {
this.showGameResult.set(true);
console.log('Game result dialog shown after delay');
});
}); });
} }
} }
@ -165,12 +173,16 @@ export default class BlackjackComponent implements OnInit {
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {
this.updateGameState(game); this.updateGameState(game);
this.userService.getCurrentUser().subscribe((user) => {
// Wait a bit to ensure the backend has finished processing
timer(1000).subscribe(() => {
const user = this.authService.currentUserValue;
if (user && user.balance < 0) { if (user && user.balance < 0) {
this.debtAmount.set(Math.abs(user.balance)); this.debtAmount.set(Math.abs(user.balance));
this.showDebtDialog.set(true); this.showDebtDialog.set(true);
} }
}); });
this.isActionInProgress.set(false); this.isActionInProgress.set(false);
}, },
error: (error) => { error: (error) => {
@ -184,7 +196,6 @@ export default class BlackjackComponent implements OnInit {
onCloseGameResult(): void { onCloseGameResult(): void {
console.log('Closing game result dialog'); console.log('Closing game result dialog');
this.showGameResult.set(false); this.showGameResult.set(false);
this.userService.refreshCurrentUser();
} }
onCloseDebtDialog(): void { onCloseDebtDialog(): void {

View file

@ -0,0 +1,16 @@
/* 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,50 +1,177 @@
<app-navbar></app-navbar> <app-navbar></app-navbar>
<div> <div class="container mx-auto px-4 py-6 space-y-8">
<h2>Payouts</h2> <h1 class="text-3xl font-bold text-white mb-6">Spielautomaten</h1>
@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> <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="grid grid-cols-3 gap-1"> <!-- Slot Machine Display -->
@for (row of slotResult().resultMatrix; track $index) { <div class="lg:col-span-3 space-y-6 flex flex-col">
@for (cell of row; track $index) { <div class="card">
<div class="text-center">{{ cell }}</div> <!-- 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="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>
<!-- 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"
[ngModel]="betAmount()"
(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()"
class="button-primary px-4 py-1.5 font-bold"
[ngClass]="{
'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> </div>
<div> <!-- Game Info Panel -->
<p> <div class="lg:col-span-1">
Game result: <strong>{{ slotResult().status | uppercase }}</strong> <div class="card p-4">
</p> <h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
<p> <div class="space-y-4">
Amount: <strong>{{ slotResult().amount }}</strong> <div class="flex justify-between items-center">
</p> <span class="text-text-secondary">Kontostand:</span>
</div> <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> <div class="grid grid-cols-2 gap-2 mb-4">
<label for="betAmount">Bet Amount: </label> <button
<input (click)="setBetAmount(0.1)"
id="betAmount" class="button-primary py-2 text-sm"
type="number" [disabled]="isSpinning"
[ngModel]="betAmount()" >
(ngModelChange)="betAmount.set($event)" 10%
step="0.01" </button>
min="0.01" <button
/> (click)="setBetAmount(0.25)"
</div> 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>
<button (click)="spin()">SPIN</button> <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>
</div> </div>

View file

@ -1,8 +1,19 @@
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 { NavbarComponent } from '@shared/components/navbar/navbar.component';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { KeyValuePipe, UpperCasePipe } from '@angular/common'; import { CommonModule, KeyValuePipe, NgClass, CurrencyPipe } from '@angular/common';
import { FormsModule } from '@angular/forms'; 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';
import { AuthService } from '@service/auth.service';
interface SlotResult { interface SlotResult {
status: 'win' | 'lose' | 'blank' | 'start'; status: 'win' | 'lose' | 'blank' | 'start';
@ -13,12 +24,25 @@ interface SlotResult {
@Component({ @Component({
selector: 'app-slots', selector: 'app-slots',
standalone: true, standalone: true,
imports: [NavbarComponent, KeyValuePipe, UpperCasePipe, FormsModule], imports: [
CommonModule,
NavbarComponent,
KeyValuePipe,
NgClass,
FormsModule,
CurrencyPipe,
AnimatedNumberComponent,
],
templateUrl: './slots.component.html', templateUrl: './slots.component.html',
styleUrl: './slots.component.css',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export default class SlotsComponent implements OnInit { export default class SlotsComponent implements OnInit, OnDestroy {
private httpClient: HttpClient = inject(HttpClient); private httpClient: HttpClient = inject(HttpClient);
private userService = inject(UserService);
private authService = inject(AuthService);
private userSubscription: Subscription | undefined;
slotInfo = signal<Record<string, number> | null>(null); slotInfo = signal<Record<string, number> | null>(null);
slotResult = signal<SlotResult>({ slotResult = signal<SlotResult>({
status: 'start', status: 'start',
@ -29,21 +53,80 @@ export default class SlotsComponent implements OnInit {
['BELL', 'BELL', 'BELL'], ['BELL', 'BELL', 'BELL'],
], ],
}); });
balance = signal<number>(0);
betAmount = signal<number>(1); betAmount = signal<number>(1);
isSpinning = false;
ngOnInit(): void { ngOnInit(): void {
this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => { this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => {
this.slotInfo.set(data); this.slotInfo.set(data);
}); });
this.userSubscription = this.authService.userSubject.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 { spin(): void {
if (!this.hasEnoughBalance()) {
return;
}
this.isSpinning = true;
const betAmount = this.betAmount();
this.userService.updateLocalBalance(-betAmount);
const payload = { 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({
this.slotResult.set(result); next: (result) => {
setTimeout(() => {
this.slotResult.set(result);
if (result.status === 'win') {
this.userService.updateLocalBalance(result.amount);
}
this.userService.refreshCurrentUser();
this.isSpinning = false;
}, 1500);
},
error: (err) => {
console.error('Error spinning slot machine:', err);
this.userService.updateLocalBalance(betAmount);
this.userService.refreshCurrentUser();
this.isSpinning = false;
},
}); });
} }
} }

View file

@ -6,6 +6,7 @@ import { LootBox, Reward } from 'app/model/LootBox';
import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { NavbarComponent } from '@shared/components/navbar/navbar.component';
import { UserService } from '@service/user.service'; import { UserService } from '@service/user.service';
import { User } from 'app/model/User'; import { User } from 'app/model/User';
import { AuthService } from '@service/auth.service';
@Component({ @Component({
selector: 'app-lootbox-opening', selector: 'app-lootbox-opening',
@ -30,10 +31,11 @@ export default class LootboxOpeningComponent {
private router: Router, private router: Router,
private lootboxService: LootboxService, private lootboxService: LootboxService,
private userService: UserService, private userService: UserService,
private authService: AuthService,
private cdr: ChangeDetectorRef private cdr: ChangeDetectorRef
) { ) {
this.loadLootbox(); this.loadLootbox();
this.userService.currentUser$.subscribe((user) => { this.authService.userSubject.subscribe((user) => {
this.currentUser = user; this.currentUser = user;
this.cdr.detectChanges(); this.cdr.detectChanges();
}); });

View file

@ -5,8 +5,9 @@ import { LootboxService } from '../services/lootbox.service';
import { LootBox } from 'app/model/LootBox'; import { LootBox } from 'app/model/LootBox';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { timeout } from 'rxjs'; import { timeout } from 'rxjs';
import { UserService } from '@service/user.service';
import { User } from 'app/model/User'; import { User } from 'app/model/User';
import { AuthService } from '@service/auth.service';
import { UserService } from '@service/user.service';
@Component({ @Component({
selector: 'app-lootbox-selection', selector: 'app-lootbox-selection',
@ -90,12 +91,13 @@ export default class LootboxSelectionComponent implements OnInit {
private lootboxService: LootboxService, private lootboxService: LootboxService,
private router: Router, private router: Router,
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private authService: AuthService,
private userService: UserService private userService: UserService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.loadLootboxes(); this.loadLootboxes();
this.userService.currentUser$.subscribe((user) => { this.authService.userSubject.subscribe((user) => {
this.currentUser = user; this.currentUser = user;
this.cdr.detectChanges(); this.cdr.detectChanges();
}); });

View file

@ -1,38 +1,33 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs';
import { User } from '../model/User';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { User } from '../model/User';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class UserService { export class UserService {
public currentUserSubject = new BehaviorSubject<User | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
private http: HttpClient = inject(HttpClient);
private authService = inject(AuthService); private authService = inject(AuthService);
public getCurrentUser(): Observable<User | null> { /**
return this.http.get<User | null>('/backend/users/me').pipe( * Updates the user's balance locally for immediate UI feedback
catchError(() => EMPTY), * This should be called before a server-side balance change is made
tap((user) => this.currentUserSubject.next(user)) * The server update will be reflected when AuthService.loadCurrentUser() is called
); */
}
public refreshCurrentUser(): void {
this.getCurrentUser().subscribe();
this.authService.loadCurrentUser();
}
public updateLocalBalance(amount: number): void { public updateLocalBalance(amount: number): void {
const currentUser = this.currentUserSubject.getValue(); const currentUser = this.authService.currentUserValue;
if (currentUser) { if (currentUser) {
const updatedUser = { const updatedUser: User = {
...currentUser, ...currentUser,
balance: currentUser.balance + amount, balance: currentUser.balance + amount,
}; };
this.currentUserSubject.next(updatedUser); this.authService.userSubject.next(updatedUser);
} }
} }
/**
* Refreshes the current user's data from the server
*/
public refreshCurrentUser(): void {
this.authService.loadCurrentUser();
}
} }