diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 05e146d..7e76ef6 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -31,3 +31,64 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + build-backend-image: + needs: release + runs-on: ubuntu-latest + name: Build Backend Image + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Ensure full history is available + - name: Extract tag + run: | + TAG=$(git describe --tags --abbrev=0) + echo "TAG=$TAG" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v4 + - name: Login + uses: docker/login-action@v3 + with: + registry: git.kjan.de + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: backend/ + file: backend/.docker/Dockerfile + push: true + tags: | + git.kjan.de/szut/casino-backend:latest + git.kjan.de/szut/casino-backend:${{ env.TAG }} + + build-frontend-image: + needs: release + runs-on: ubuntu-latest + name: Build Frontend Image + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Ensure full history is available + - name: Extract tag + run: | + TAG=$(git describe --tags --abbrev=0) + echo "TAG=$TAG" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v4 + - name: Login + uses: docker/login-action@v3 + with: + registry: git.kjan.de + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: frontend/ + file: frontend/.docker/Dockerfile + push: true + tags: | + git.kjan.de/szut/casino-frontend:latest + git.kjan.de/szut/casino-frontend:${{ env.TAG }} diff --git a/backend/.docker/Dockerfile b/backend/.docker/Dockerfile index d6df4f7..cbb743f 100644 --- a/backend/.docker/Dockerfile +++ b/backend/.docker/Dockerfile @@ -1,17 +1,17 @@ -FROM gradle:jdk22 AS builder +FROM gradle:jdk23 AS builder WORKDIR /app -COPY gradlew build.gradle.kts settings.gradle.kts ./ +COPY gradlew build.gradle.kts settings.gradle.kts config ./ COPY gradle gradle RUN chmod +x gradlew -RUN ./gradlew dependencies +RUN gradle dependencies COPY src src -RUN ./gradlew clean build -x test +RUN gradle clean build -x test -x checkstyleMain -x checkstyleTest -x compileTestJava -FROM openjdk:22-jdk-slim +FROM openjdk:23-jdk-slim AS runtime WORKDIR /app COPY --from=builder /app/build/libs/*.jar app.jar diff --git a/backend/src/main/java/de/szut/casino/CasinoApplication.java b/backend/src/main/java/de/szut/casino/CasinoApplication.java index 68e2ebe..8c99f02 100644 --- a/backend/src/main/java/de/szut/casino/CasinoApplication.java +++ b/backend/src/main/java/de/szut/casino/CasinoApplication.java @@ -1,20 +1,10 @@ package de.szut.casino; -import de.szut.casino.lootboxes.LootBoxEntity; -import de.szut.casino.lootboxes.LootBoxRepository; -import de.szut.casino.lootboxes.RewardEntity; -import de.szut.casino.lootboxes.RewardRepository; -import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - @SpringBootApplication public class CasinoApplication { diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java index c89f3ef..c9d5c26 100644 --- a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameController.java @@ -1,6 +1,7 @@ package de.szut.casino.blackjack; -import de.szut.casino.blackjack.dto.CreateBlackJackGameDto; +import de.szut.casino.shared.dto.BetDto; +import de.szut.casino.shared.service.BalanceService; import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserService; import jakarta.validation.Valid; @@ -9,19 +10,18 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; @Slf4j @RestController public class BlackJackGameController { + private final BalanceService balanceService; private final UserService userService; private final BlackJackService blackJackService; - public BlackJackGameController(UserService userService, BlackJackService blackJackService) { + public BlackJackGameController(BalanceService balanceService, UserService userService, BlackJackService blackJackService) { + this.balanceService = balanceService; this.blackJackService = blackJackService; this.userService = userService; } @@ -112,7 +112,7 @@ public class BlackJackGameController { } @PostMapping("/blackjack/start") - public ResponseEntity createBlackJackGame(@RequestBody @Valid CreateBlackJackGameDto createBlackJackGameDto, @RequestHeader("Authorization") String token) { + public ResponseEntity createBlackJackGame(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { Optional optionalUser = userService.getCurrentUser(token); if (optionalUser.isEmpty()) { @@ -120,21 +120,11 @@ public class BlackJackGameController { } UserEntity user = optionalUser.get(); - BigDecimal balance = user.getBalance(); - BigDecimal betAmount = createBlackJackGameDto.getBetAmount(); - if (betAmount.compareTo(BigDecimal.ZERO) <= 0) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Invalid bet amount"); - return ResponseEntity.badRequest().body(errorResponse); + if (!this.balanceService.hasFunds(user, betDto)) { + return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Insufficient funds")); } - if (betAmount.compareTo(balance) > 0) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Insufficient funds"); - return ResponseEntity.badRequest().body(errorResponse); - } - - return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betAmount)); + return ResponseEntity.ok(blackJackService.createBlackJackGame(user, betDto.getBetAmount())); } } diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameRepository.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameRepository.java index d25a180..aafea4f 100644 --- a/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameRepository.java +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackGameRepository.java @@ -1,12 +1,8 @@ package de.szut.casino.blackjack; -import de.szut.casino.user.UserEntity; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Service; -import java.util.Optional; - @Service public interface BlackJackGameRepository extends JpaRepository { } diff --git a/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java index 158e32c..cb31352 100644 --- a/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java +++ b/backend/src/main/java/de/szut/casino/blackjack/BlackJackService.java @@ -7,7 +7,6 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.List; -import java.util.Optional; import java.util.Random; @Service diff --git a/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java b/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java deleted file mode 100644 index e5b0c97..0000000 --- a/backend/src/main/java/de/szut/casino/blackjack/dto/CreateBlackJackGameDto.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.szut.casino.blackjack.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.math.BigDecimal; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -public class CreateBlackJackGameDto { - private BigDecimal betAmount; -} diff --git a/backend/src/main/java/de/szut/casino/deposit/DepositController.java b/backend/src/main/java/de/szut/casino/deposit/DepositController.java index cdf883d..dbfe449 100644 --- a/backend/src/main/java/de/szut/casino/deposit/DepositController.java +++ b/backend/src/main/java/de/szut/casino/deposit/DepositController.java @@ -8,7 +8,6 @@ import de.szut.casino.deposit.dto.AmountDto; import de.szut.casino.deposit.dto.SessionIdDto; import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserRepository; -import de.szut.casino.user.UserService; import de.szut.casino.user.dto.KeycloakUserDto; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Value; @@ -16,7 +15,10 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.Optional; diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java b/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java index e3fc412..7c43af9 100644 --- a/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionEntity.java @@ -5,7 +5,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; -import java.math.BigDecimal; +import java.util.Date; @Setter @Getter @@ -26,4 +26,7 @@ public class TransactionEntity { @Enumerated(EnumType.STRING) private TransactionStatus status = TransactionStatus.PROCESSING; + + @Column(name = "created_at") + private Date createdAt = new Date(); } diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java b/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java index 94b9b6b..5a16f0d 100644 --- a/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionRepository.java @@ -5,10 +5,20 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Service; +import java.util.List; import java.util.Optional; @Service public interface TransactionRepository extends JpaRepository { @Query("SELECT t FROM TransactionEntity t WHERE t.sessionId = ?1") Optional findOneBySessionID(String sessionId); + + @Query("SELECT t FROM TransactionEntity t WHERE t.user = ?1") + List findAllByUserId(UserEntity id); + + @Query("SELECT t FROM TransactionEntity t WHERE t.user = ?1 ORDER BY t.createdAt DESC LIMIT ?2 OFFSET ?3") + List findByUserIdWithLimit(UserEntity userEntity, Integer limit, Integer offset); + + @Query("SELECT COUNT(t) > ?2 + ?3 FROM TransactionEntity t WHERE t.user = ?1") + Boolean hasMore(UserEntity userEntity, Integer limit, Integer offset); } diff --git a/backend/src/main/java/de/szut/casino/deposit/TransactionService.java b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java index b5ddfd2..fceb27b 100644 --- a/backend/src/main/java/de/szut/casino/deposit/TransactionService.java +++ b/backend/src/main/java/de/szut/casino/deposit/TransactionService.java @@ -7,7 +7,7 @@ import de.szut.casino.user.UserEntity; import de.szut.casino.user.UserRepository; import org.springframework.stereotype.Service; -import java.util.Objects; +import java.math.BigDecimal; import java.util.Optional; @Service @@ -55,7 +55,7 @@ public class TransactionService { UserEntity user = transaction.getUser(); Long amountTotal = checkoutSession.getAmountTotal(); if (amountTotal != null) { - user.addBalance(amountTotal); + user.addBalance(BigDecimal.valueOf(amountTotal).movePointLeft(2)); } userRepository.save(user); diff --git a/backend/src/main/java/de/szut/casino/deposit/WebhookController.java b/backend/src/main/java/de/szut/casino/deposit/WebhookController.java index be90a48..dba9041 100644 --- a/backend/src/main/java/de/szut/casino/deposit/WebhookController.java +++ b/backend/src/main/java/de/szut/casino/deposit/WebhookController.java @@ -1,26 +1,21 @@ package de.szut.casino.deposit; -import com.fasterxml.jackson.core.JsonProcessingException; - import com.stripe.Stripe; -import com.stripe.exception.SignatureVerificationException; import com.stripe.exception.StripeException; -import com.stripe.model.*; +import com.stripe.model.Event; import com.stripe.model.checkout.Session; import com.stripe.net.Webhook; -import com.stripe.param.checkout.SessionRetrieveParams; -import de.szut.casino.user.UserEntity; -import de.szut.casino.user.UserRepository; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; -import java.math.BigDecimal; import java.util.Objects; -import java.util.Optional; @RestController public class WebhookController { diff --git a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxEntity.java b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxEntity.java index eb5b93a..8a3e9a9 100644 --- a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxEntity.java +++ b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxEntity.java @@ -1,15 +1,9 @@ package de.szut.casino.lootboxes; -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import de.szut.casino.blackjack.CardEntity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.SQLRestriction; import java.math.BigDecimal; import java.util.ArrayList; diff --git a/backend/src/main/java/de/szut/casino/lootboxes/RewardEntity.java b/backend/src/main/java/de/szut/casino/lootboxes/RewardEntity.java index c69390c..1abd2df 100644 --- a/backend/src/main/java/de/szut/casino/lootboxes/RewardEntity.java +++ b/backend/src/main/java/de/szut/casino/lootboxes/RewardEntity.java @@ -1,7 +1,6 @@ package de.szut.casino.lootboxes; import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/backend/src/main/java/de/szut/casino/shared/dto/BetDto.java b/backend/src/main/java/de/szut/casino/shared/dto/BetDto.java new file mode 100644 index 0000000..a910a03 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/shared/dto/BetDto.java @@ -0,0 +1,18 @@ +package de.szut.casino.shared.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +@Getter +@Setter +@AllArgsConstructor +public class BetDto { + @NotNull(message = "Bet amount cannot be null") + @Positive(message = "Bet amount must be positive") + private BigDecimal betAmount; +} diff --git a/backend/src/main/java/de/szut/casino/shared/service/BalanceService.java b/backend/src/main/java/de/szut/casino/shared/service/BalanceService.java new file mode 100644 index 0000000..40e6caa --- /dev/null +++ b/backend/src/main/java/de/szut/casino/shared/service/BalanceService.java @@ -0,0 +1,36 @@ +package de.szut.casino.shared.service; + +import de.szut.casino.shared.dto.BetDto; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class BalanceService { + private UserRepository userRepository; + + public BalanceService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public boolean hasFunds(UserEntity user, BetDto betDto) { + BigDecimal balance = user.getBalance(); + BigDecimal betAmount = betDto.getBetAmount(); + + return betAmount.compareTo(balance) <= 0; + } + + public void addFunds(UserEntity user, BigDecimal amount) { + user.addBalance(amount); + + this.userRepository.save(user); + } + + public void subtractFunds(UserEntity user, BigDecimal amount) { + user.subtractBalance(amount); + + this.userRepository.save(user); + } +} diff --git a/backend/src/main/java/de/szut/casino/slots/SlotController.java b/backend/src/main/java/de/szut/casino/slots/SlotController.java new file mode 100644 index 0000000..8f98b1d --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/SlotController.java @@ -0,0 +1,50 @@ +package de.szut.casino.slots; + +import de.szut.casino.shared.dto.BetDto; +import de.szut.casino.shared.service.BalanceService; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.Optional; + +@RestController +public class SlotController { + private final UserService userService; + private final BalanceService balanceService; + private final SlotService slotService; + + public SlotController(UserService userService, BalanceService balanceService, SlotService slotService) { + this.userService = userService; + this.balanceService = balanceService; + this.slotService = slotService; + } + + @PostMapping("/slots/spin") + public ResponseEntity spinSlots(@RequestBody @Valid BetDto betDto, @RequestHeader("Authorization") String token) { + Optional optionalUser = userService.getCurrentUser(token); + + if (optionalUser.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + UserEntity user = optionalUser.get(); + + if (!this.balanceService.hasFunds(user, betDto)) { + return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Insufficient funds")); + } + + SpinResult spinResult = this.slotService.spin( + betDto.getBetAmount(), + user + ); + + return ResponseEntity.ok(spinResult); + } +} diff --git a/backend/src/main/java/de/szut/casino/slots/SlotService.java b/backend/src/main/java/de/szut/casino/slots/SlotService.java new file mode 100644 index 0000000..39ebeab --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/SlotService.java @@ -0,0 +1,139 @@ +package de.szut.casino.slots; + +import de.szut.casino.shared.service.BalanceService; +import de.szut.casino.user.UserEntity; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +@Service +public class SlotService { + private final int REEL_LENGTH = 32; + + // 98% RTP + private final int SEVEN_COUNT = 1; + private final int BAR_COUNT = 4; + private final int BELL_COUNT = 7; + private final int CHERRY_COUNT = 10; + private final int BLANK_COUNT = 10; + + private final Symbol SEVEN = new Symbol("seven", new BigDecimal("1000")); + private final Symbol BAR = new Symbol("bar", new BigDecimal("85")); + private final Symbol BELL = new Symbol("bell", new BigDecimal("40")); + private final Symbol CHERRY = new Symbol("cherry", new BigDecimal("10")); + private final Symbol BLANK = new Symbol("blank", new BigDecimal("0")); + + private final List firstReel; + private final List secondReel; + private final List thirdReel; + + private final Random random; + private final BalanceService balanceService; + + public SlotService(BalanceService balanceService) { + this.random = new Random(); + this.balanceService = balanceService; + + List reelStrip = createReelStrip(); + this.firstReel = shuffleReel(reelStrip); + this.secondReel = shuffleReel(reelStrip); + this.thirdReel = shuffleReel(reelStrip); + } + + public SpinResult spin(BigDecimal betAmount, UserEntity user) { + int index1 = this.random.nextInt(REEL_LENGTH); + int index2 = this.random.nextInt(REEL_LENGTH); + int index3 = this.random.nextInt(REEL_LENGTH); + + Symbol symbol1 = getSymbolAt(this.firstReel, index1); + Symbol symbol2 = getSymbolAt(this.secondReel, index2); + Symbol symbol3 = getSymbolAt(this.thirdReel, index3); + + boolean isWin = symbol1.equals(symbol2) && symbol1.equals(symbol3); + + SpinResult spinResult = processResult(betAmount, user, isWin, symbol1); + buildResultMatrix(spinResult, index1, index2, index3); + + return spinResult; + } + + private SpinResult processResult(BigDecimal betAmount, UserEntity user, boolean isWin, Symbol winSymbol) { + BigDecimal resultAmount; + String status; + + if (isWin) { + resultAmount = betAmount.multiply(winSymbol.getPayoutMultiplier()); + status = "win"; + this.balanceService.addFunds(user, resultAmount); + } else { + resultAmount = betAmount; + status = "lose"; + this.balanceService.subtractFunds(user, betAmount); + } + + SpinResult spinResult = new SpinResult(); + spinResult.setStatus(status); + spinResult.setAmount(resultAmount); + spinResult.setWin(isWin); + + return spinResult; + } + + private void buildResultMatrix(SpinResult spinResult, int index1, int index2, int index3) { + List> resultMatrix = new ArrayList<>(3); + + for (int i = 0; i < 3; i++) { + resultMatrix.add(new ArrayList<>(3)); + } + + resultMatrix.getFirst().add(getSymbolAt(this.firstReel, index1 - 1)); + resultMatrix.getFirst().add(getSymbolAt(this.secondReel, index2 - 1)); + resultMatrix.getFirst().add(getSymbolAt(this.thirdReel, index3 - 1)); + + resultMatrix.get(1).add(getSymbolAt(this.firstReel, index1)); + resultMatrix.get(1).add(getSymbolAt(this.secondReel, index2)); + resultMatrix.get(1).add(getSymbolAt(this.thirdReel, index3)); + + resultMatrix.getLast().add(getSymbolAt(this.firstReel, index1 + 1)); + resultMatrix.getLast().add(getSymbolAt(this.secondReel, index2 + 1)); + resultMatrix.getLast().add(getSymbolAt(this.thirdReel, index3 + 1)); + + spinResult.setResultMatrix(resultMatrix); + } + + private List shuffleReel(List reelStrip) { + Collections.shuffle(reelStrip, this.random); + + return reelStrip; + } + + private List createReelStrip() { + List reelStrip = new ArrayList<>(REEL_LENGTH); + addSymbolsToStrip(reelStrip, CHERRY, CHERRY_COUNT); + addSymbolsToStrip(reelStrip, BELL, BELL_COUNT); + addSymbolsToStrip(reelStrip, BAR, BAR_COUNT); + addSymbolsToStrip(reelStrip, SEVEN, SEVEN_COUNT); + addSymbolsToStrip(reelStrip, BLANK, BLANK_COUNT); + return reelStrip; + } + + private void addSymbolsToStrip(List strip, Symbol symbol, int count) { + for (int i = 0; i < count; i++) { + strip.add(symbol); + } + } + + private Symbol getSymbolAt(List reel, int index) { + int effectiveIndex = index % REEL_LENGTH; + + if (effectiveIndex < 0) { + effectiveIndex += REEL_LENGTH; + } + + return reel.get(effectiveIndex); + } +} diff --git a/backend/src/main/java/de/szut/casino/slots/SpinResult.java b/backend/src/main/java/de/szut/casino/slots/SpinResult.java new file mode 100644 index 0000000..6263e9b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/SpinResult.java @@ -0,0 +1,24 @@ +package de.szut.casino.slots; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class SpinResult { + public SpinResult(String status, BigDecimal amount, boolean isWin) { + this.status = status; + this.amount = amount; + this.isWin = isWin; + } + + private String status; + private BigDecimal amount; + private boolean isWin; + private List> resultMatrix; +} diff --git a/backend/src/main/java/de/szut/casino/slots/Symbol.java b/backend/src/main/java/de/szut/casino/slots/Symbol.java new file mode 100644 index 0000000..806d14b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/slots/Symbol.java @@ -0,0 +1,35 @@ +package de.szut.casino.slots; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +@Getter +@Setter +@AllArgsConstructor +public class Symbol { + private String name; + private BigDecimal payoutMultiplier; + + @Override + public boolean equals(Object other) { + if (!(other instanceof Symbol that)) { + return false; + } + + return this.name.equals(that.name) && this.payoutMultiplier.equals(that.payoutMultiplier); + } + + @Override + public int hashCode() { + int hashCode = 1; + + hashCode = hashCode * 37 + this.name.hashCode(); + hashCode = hashCode * 37 + this.payoutMultiplier.hashCode(); + + return hashCode; + } +} diff --git a/backend/src/main/java/de/szut/casino/user/UserController.java b/backend/src/main/java/de/szut/casino/user/UserController.java index 5daf7ef..c2ad0d0 100644 --- a/backend/src/main/java/de/szut/casino/user/UserController.java +++ b/backend/src/main/java/de/szut/casino/user/UserController.java @@ -1,19 +1,14 @@ package de.szut.casino.user; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RestController; - import de.szut.casino.user.dto.CreateUserDto; import de.szut.casino.user.dto.GetUserDto; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; @Slf4j @RestController diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java index 67fc1ae..03f4a34 100644 --- a/backend/src/main/java/de/szut/casino/user/UserEntity.java +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -31,13 +31,31 @@ public class UserEntity { this.balance = balance; } - public void addBalance(long amountInCents) { - BigDecimal amountToAdd = BigDecimal.valueOf(amountInCents).movePointLeft(2); + public void addBalance(BigDecimal amountToAdd) { + if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) <= 0) { + return; + } if (this.balance == null) { - this.balance = amountToAdd; - } else { - this.balance = this.balance.add(amountToAdd); + this.balance = BigDecimal.ZERO; } + + this.balance = this.balance.add(amountToAdd); + } + + public void subtractBalance(BigDecimal amountToSubtract) { + if (amountToSubtract == null || amountToSubtract.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Amount to subtract must be positive."); + } + + if (this.balance == null) { + this.balance = BigDecimal.ZERO; + } + + if (this.balance.compareTo(amountToSubtract) < 0) { + throw new IllegalStateException("Insufficient funds to subtract " + amountToSubtract); + } + + this.balance = this.balance.subtract(amountToSubtract); } } diff --git a/backend/src/main/java/de/szut/casino/user/transaction/GetTransactionService.java b/backend/src/main/java/de/szut/casino/user/transaction/GetTransactionService.java new file mode 100644 index 0000000..831c438 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/transaction/GetTransactionService.java @@ -0,0 +1,45 @@ +package de.szut.casino.user.transaction; + +import de.szut.casino.deposit.TransactionEntity; +import de.szut.casino.deposit.TransactionRepository; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserService; +import de.szut.casino.user.transaction.dto.GetTransactionDto; +import de.szut.casino.user.transaction.dto.UserTransactionsDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class GetTransactionService { + + @Autowired + private UserService userService; + + @Autowired + private TransactionRepository transactionRepository; + + public UserTransactionsDto getUserTransactionsDto(String authToken, Integer limit, Integer offset) { + Optional user = this.userService.getCurrentUser(authToken); + if (user.isPresent()) { + List transactionEntities = this.transactionRepository.findByUserIdWithLimit(user.get(), limit, offset); + Boolean hasMore = this.transactionRepository.hasMore(user.get(), limit, offset); + + return new UserTransactionsDto(mapTransactionsToDtos(transactionEntities), hasMore); + } + + return new UserTransactionsDto(List.of(), false); + } + + public List mapTransactionsToDtos(List transactions) { + return transactions.stream() + .map(transaction -> new GetTransactionDto( + transaction.getAmount(), + transaction.getStatus(), + transaction.getCreatedAt()) + ).toList(); + } +} + diff --git a/backend/src/main/java/de/szut/casino/user/transaction/TransactionController.java b/backend/src/main/java/de/szut/casino/user/transaction/TransactionController.java new file mode 100644 index 0000000..10061fa --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/transaction/TransactionController.java @@ -0,0 +1,28 @@ +package de.szut.casino.user.transaction; + +import de.szut.casino.user.transaction.dto.UserTransactionsDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TransactionController { + + @Autowired + private GetTransactionService transactionService; + + @GetMapping("/user/transactions") + public ResponseEntity getUserTransactions( + @RequestHeader("Authorization") String authToken, + @RequestParam(value = "limit", required = false) Integer limit, + @RequestParam(value = "offset", required = false) Integer offset + ) { + UserTransactionsDto transactionEntities = this.transactionService.getUserTransactionsDto(authToken, limit, offset); + + return ResponseEntity.ok(transactionEntities); + } +} + diff --git a/backend/src/main/java/de/szut/casino/user/transaction/dto/GetTransactionDto.java b/backend/src/main/java/de/szut/casino/user/transaction/dto/GetTransactionDto.java new file mode 100644 index 0000000..f37fbf4 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/transaction/dto/GetTransactionDto.java @@ -0,0 +1,16 @@ +package de.szut.casino.user.transaction.dto; + +import de.szut.casino.deposit.TransactionStatus; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@AllArgsConstructor +@NoArgsConstructor +public class GetTransactionDto { + public double amount = 0; + public TransactionStatus status = TransactionStatus.PROCESSING; + public Date createdAt = new Date(); +} + diff --git a/backend/src/main/java/de/szut/casino/user/transaction/dto/UserTransactionsDto.java b/backend/src/main/java/de/szut/casino/user/transaction/dto/UserTransactionsDto.java new file mode 100644 index 0000000..54e116b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/transaction/dto/UserTransactionsDto.java @@ -0,0 +1,12 @@ +package de.szut.casino.user.transaction.dto; + +import lombok.AllArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +public class UserTransactionsDto { + public List transactions; + public Boolean hasMore; +} + diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 4c56a9d..bb8a4be 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,17 +1,17 @@ -spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/postgresdb -spring.datasource.username=postgres_user -spring.datasource.password=postgres_pass -server.port=8080 +spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:postgresdb} +spring.datasource.username=${DB_USER:postgres_user} +spring.datasource.password=${DB_PASS:postgres_pass} +server.port=${HTTP_PORT:8080} spring.jpa.hibernate.ddl-auto=update stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I} -stripe.webhook.secret=whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15 -app.frontend-host=http://localhost:4200 +stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15} +app.frontend-host=${FE_URL:http://localhost:4200} -spring.application.name=lf12_starter +spring.application.name=casino #client registration configuration -spring.security.oauth2.client.registration.authentik.client-id=MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm -spring.security.oauth2.client.registration.authentik.client-secret=GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5 +spring.security.oauth2.client.registration.authentik.client-id=${AUTH_CLIENT_ID:MDqjm1kcWKuZfqHJXjxwAV20i44aT7m4VhhTL3Nm} +spring.security.oauth2.client.registration.authentik.client-secret=${AUTH_CLIENT_SECRET:GY2F8te6iAVYt1TNAUVLzWZEXb6JoMNp6chbjqaXNq4gS5xTDL54HqBiAlV1jFKarN28LQ7FUsYX4SbwjfEhZhgeoKuBnZKjR9eiu7RawnGgxIK9ffvUfMkjRxnmiGI5} spring.security.oauth2.client.registration.authentik.provider=authentik spring.security.oauth2.client.registration.authentik.client-name=Authentik spring.security.oauth2.client.registration.authentik.scope=openid,email,profile @@ -20,16 +20,16 @@ spring.security.oauth2.client.registration.authentik.authorization-grant-type=au spring.security.oauth2.client.registration.authentik.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} # Provider settings -spring.security.oauth2.client.provider.authentik.issuer-uri=https://oauth.simonis.lol/application/o/casino-dev/ -spring.security.oauth2.client.provider.authentik.authorization-uri=https://oauth.simonis.lol/application/o/authorize/ -spring.security.oauth2.client.provider.authentik.token-uri=https://oauth.simonis.lol/application/o/token/ -spring.security.oauth2.client.provider.authentik.user-info-uri=https://oauth.simonis.lol/application/o/userinfo/ -spring.security.oauth2.client.provider.authentik.jwk-set-uri=https://oauth.simonis.lol/application/o/casino-dev/jwks/ -spring.security.oauth2.client.provider.authentik.user-name-attribute=preferred_username +spring.security.oauth2.client.provider.authentik.issuer-uri=${AUTH_PROVIDER_ISSUER:https://oauth.simonis.lol/application/o/casino-dev/} +spring.security.oauth2.client.provider.authentik.authorization-uri=${AUTH_PROVIDER_AUTHORIZE_URI:https://oauth.simonis.lol/application/o/authorize/} +spring.security.oauth2.client.provider.authentik.token-uri=${AUTH_PROVIDER_TOKEN_URI:https://oauth.simonis.lol/application/o/token/} +spring.security.oauth2.client.provider.authentik.user-info-uri=${AUTH_PROVIDER_USERINFO_URI:https://oauth.simonis.lol/application/o/userinfo/} +spring.security.oauth2.client.provider.authentik.jwk-set-uri=${AUTH_PROVIDER_JWKS_URI:https://oauth.simonis.lol/application/o/casino-dev/jwks/} +spring.security.oauth2.client.provider.authentik.user-name-attribute=${AUTH_PROVIDER_NAME_ATTR:preferred_username} # Resource server config -spring.security.oauth2.resourceserver.jwt.issuer-uri=https://oauth.simonis.lol/application/o/casino-dev/ -spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://oauth.simonis.lol/application/o/casino-dev/jwks/ +spring.security.oauth2.resourceserver.jwt.issuer-uri=${AUTH_JWT_ISSUER_URI:https://oauth.simonis.lol/application/o/casino-dev}/ +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${AUTH_JWT_JWT_SET_URI:https://oauth.simonis.lol/application/o/casino-dev/jwks/} #OIDC provider configuration: logging.level.org.springframework.security=DEBUG diff --git a/frontend/.docker/Dockerfile b/frontend/.docker/Dockerfile new file mode 100644 index 0000000..494c91f --- /dev/null +++ b/frontend/.docker/Dockerfile @@ -0,0 +1,23 @@ +FROM oven/bun:debian AS build +WORKDIR /app + +RUN apt-get update -y && apt-get install nodejs -y + +ENV NODE_ENV=production + +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +COPY . . +RUN bun run build + +FROM nginx:alpine AS production + +RUN rm /etc/nginx/conf.d/default.conf +COPY .docker/casino.conf /etc/nginx/templates/nginx.conf.template +COPY .docker/entrypoint.sh /docker-entrypoint.d/40-custom-config-env.sh + +COPY --from=build /app/dist/casino /usr/share/nginx/html + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/.docker/casino.conf b/frontend/.docker/casino.conf new file mode 100644 index 0000000..40b9613 --- /dev/null +++ b/frontend/.docker/casino.conf @@ -0,0 +1,19 @@ +server { + listen 80; + root /usr/share/nginx/html/browser; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache"; + } + + location /backend/ { + proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT}/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/frontend/.docker/entrypoint.sh b/frontend/.docker/entrypoint.sh new file mode 100755 index 0000000..3842b5a --- /dev/null +++ b/frontend/.docker/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# Default values if not provided +: ${BACKEND_HOST:=localhost} +: ${BACKEND_PORT:=8080} + +envsubst '$BACKEND_HOST $BACKEND_PORT' < /etc/nginx/templates/nginx.conf.template > /etc/nginx/conf.d/default.conf +exec nginx -g 'daemon off;' diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..4e10341 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +dist +.angular +.git +.github +.vscode +.idea +*.md +!README.md +.DS_Store +.env* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +bun-debug.log* \ No newline at end of file diff --git a/frontend/angular.json b/frontend/angular.json index f7a1430..a9f9a84 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -13,7 +13,7 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist/lf10-starter2024", + "outputPath": "dist/casino", "index": "src/index.html", "browser": "src/main.ts", "tsConfig": "tsconfig.app.json", diff --git a/frontend/docker/docker-compose.yml b/frontend/docker/docker-compose.yml deleted file mode 100644 index 2123d63..0000000 --- a/frontend/docker/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3' - -volumes: - employee_postgres_data: - driver: local - -services: - postgres-employee: - container_name: postgres_employee - image: postgres:17.4 - volumes: - - employee_postgres_data:/var/lib/postgresql/data - environment: - POSTGRES_DB: employee_db - POSTGRES_USER: employee - POSTGRES_PASSWORD: secret - ports: - - "5432:5432" - - employee: - container_name: employee - image: berndheidemann/employee-management-service:1.1.3 - # image: berndheidemann/employee-management-service_without_keycloak:1.1 - environment: - spring.datasource.url: jdbc:postgresql://postgres-employee:5432/employee_db - spring.datasource.username: employee - spring.datasource.password: secret - ports: - - "8089:8089" - depends_on: - - postgres-employee diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 671a717..d6ac35a 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -6,7 +6,7 @@ import { routes } from './app.routes'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { OAuthStorage, provideOAuthClient } from 'angular-oauth2-oidc'; -import { httpInterceptor } from './shared/interceptor/http.interceptor'; +import { httpInterceptor } from '@shared/interceptor/http.interceptor'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/frontend/src/app/feature/deposit/deposit.component.ts b/frontend/src/app/feature/deposit/deposit.component.ts index 834a2e4..8768bbe 100644 --- a/frontend/src/app/feature/deposit/deposit.component.ts +++ b/frontend/src/app/feature/deposit/deposit.component.ts @@ -1,18 +1,18 @@ import { + AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, EventEmitter, inject, Input, + OnChanges, + OnDestroy, OnInit, Output, - ViewChild, - AfterViewInit, - OnDestroy, - OnChanges, SimpleChanges, - ChangeDetectorRef, + ViewChild, } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { loadStripe, Stripe } from '@stripe/stripe-js'; diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.ts b/frontend/src/app/feature/game/blackjack/blackjack.component.ts index 370b535..3e58e25 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.ts +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, inject, signal, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { PlayingCardComponent } from './components/playing-card/playing-card.component'; @@ -6,7 +6,7 @@ import { DealerHandComponent } from './components/dealer-hand/dealer-hand.compon import { PlayerHandComponent } from './components/player-hand/player-hand.component'; import { GameControlsComponent } from './components/game-controls/game-controls.component'; import { GameInfoComponent } from './components/game-info/game-info.component'; -import { Card, BlackjackGame } from '@blackjack/models/blackjack.model'; +import { BlackjackGame, Card } from '@blackjack/models/blackjack.model'; import { BlackjackService } from '@blackjack/services/blackjack.service'; import { HttpErrorResponse } from '@angular/common/http'; import { GameResultComponent } from '@blackjack/components/game-result/game-result.component'; 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 index 25ea0e4..7d78871 100644 --- 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 @@ -1,12 +1,12 @@ import { + AfterViewInit, ChangeDetectionStrategy, Component, + ElementRef, Input, OnChanges, SimpleChanges, - ElementRef, ViewChild, - AfterViewInit, } from '@angular/core'; import { CommonModule, CurrencyPipe } from '@angular/common'; import { CountUp } from 'countup.js'; 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 ecd1fad..644fb22 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 @@ -5,8 +5,8 @@ import { Input, OnChanges, Output, - SimpleChanges, signal, + SimpleChanges, } from '@angular/core'; import { CommonModule, CurrencyPipe } from '@angular/common'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; 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 b841ee8..74d02e4 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 @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { CommonModule, CurrencyPipe } from '@angular/common'; import { animate, style, transition, trigger } from '@angular/animations'; import { GameState } from '../../enum/gameState'; diff --git a/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts b/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts index 8ae8824..0fbbb5a 100644 --- a/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts @@ -1,9 +1,9 @@ import { + AfterViewInit, ChangeDetectionStrategy, Component, - Input, - AfterViewInit, ElementRef, + Input, OnChanges, SimpleChanges, } from '@angular/core'; diff --git a/frontend/src/app/feature/game/blackjack/services/blackjack.service.ts b/frontend/src/app/feature/game/blackjack/services/blackjack.service.ts index ca3f218..5e43dc7 100644 --- a/frontend/src/app/feature/game/blackjack/services/blackjack.service.ts +++ b/frontend/src/app/feature/game/blackjack/services/blackjack.service.ts @@ -1,6 +1,6 @@ -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable, catchError } from 'rxjs'; +import { catchError, Observable } from 'rxjs'; import { BlackjackGame } from '@blackjack/models/blackjack.model'; @Injectable({ diff --git a/frontend/src/app/feature/home/home.component.html b/frontend/src/app/feature/home/home.component.html index b1bfd9b..dc8ee59 100644 --- a/frontend/src/app/feature/home/home.component.html +++ b/frontend/src/app/feature/home/home.component.html @@ -81,12 +81,16 @@ [isOpen]="isDepositModalOpen" (closeModalEmitter)="closeDepositModal()" > - - + @@ -100,11 +104,13 @@
-

{{ transaction.type }}

-

{{ transaction.date }}

+

{{ transaction.status }}

+

+ {{ transaction.createdAt | date: 'd.m.Y H:m' }} +

{{ transaction.amount | currency: 'EUR' }} diff --git a/frontend/src/app/feature/home/home.component.ts b/frontend/src/app/feature/home/home.component.ts index eb12454..be84450 100644 --- a/frontend/src/app/feature/home/home.component.ts +++ b/frontend/src/app/feature/home/home.component.ts @@ -1,22 +1,36 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { CurrencyPipe, NgFor } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { AsyncPipe, CurrencyPipe, DatePipe, NgFor } from '@angular/common'; import { DepositComponent } from '../deposit/deposit.component'; import { ActivatedRoute, Router } from '@angular/router'; import { ConfirmationComponent } from '@shared/components/confirmation/confirmation.component'; -import { Transaction } from 'app/model/Transaction'; import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { Game } from 'app/model/Game'; +import { Observable } from 'rxjs'; +import { TransactionService } from '@service/transaction.service'; +import format from 'ajv/dist/vocabularies/format'; +import { TransactionHistoryComponent } from '../transaction-history/transaction-history.component'; +import { TransactionData } from '../../model/TransactionData'; @Component({ selector: 'app-homepage', standalone: true, - imports: [NavbarComponent, CurrencyPipe, NgFor, DepositComponent, ConfirmationComponent], + imports: [ + NavbarComponent, + CurrencyPipe, + NgFor, + DepositComponent, + ConfirmationComponent, + AsyncPipe, + DatePipe, + TransactionHistoryComponent, + ], templateUrl: './home.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export default class HomeComponent implements OnInit { isDepositModalOpen = false; isDepositSuccessful = false; + isTransactionModalOpen = false; constructor( public route: ActivatedRoute, @@ -74,11 +88,13 @@ export default class HomeComponent implements OnInit { allGames: Game[] = [...this.featuredGames]; - recentTransactions: Transaction[] = []; + recentTransactionData: Observable = + inject(TransactionService).getUsersTransactions(5); openDepositModal() { this.isDepositModalOpen = true; } + closeDepositModal() { this.isDepositModalOpen = false; } @@ -86,11 +102,22 @@ export default class HomeComponent implements OnInit { openDepositConfirmationModal() { this.isDepositSuccessful = true; } + + openTransactionModal() { + this.isTransactionModalOpen = true; + } + closeDepositConfirmationModal() { this.isDepositSuccessful = false; } + closeTransactionModal() { + this.isTransactionModalOpen = false; + } + navigateToGame(route: string) { this.router.navigate([route]); } + + protected readonly format = format; } diff --git a/frontend/src/app/feature/landing/landing.component.ts b/frontend/src/app/feature/landing/landing.component.ts index 62fa25e..915547f 100644 --- a/frontend/src/app/feature/landing/landing.component.ts +++ b/frontend/src/app/feature/landing/landing.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { NgFor } from '@angular/common'; import { NavbarComponent } from '@shared/components/navbar/navbar.component'; diff --git a/frontend/src/app/feature/transaction-history/transaction-history.component.css b/frontend/src/app/feature/transaction-history/transaction-history.component.css new file mode 100644 index 0000000..c68e294 --- /dev/null +++ b/frontend/src/app/feature/transaction-history/transaction-history.component.css @@ -0,0 +1,8 @@ +button[disabled] { + cursor: not-allowed; + background-color: #077b58; + box-shadow: none; +} +button[disabled]:hover { + background-color: #077b58; +} diff --git a/frontend/src/app/feature/transaction-history/transaction-history.component.html b/frontend/src/app/feature/transaction-history/transaction-history.component.html new file mode 100644 index 0000000..974964c --- /dev/null +++ b/frontend/src/app/feature/transaction-history/transaction-history.component.html @@ -0,0 +1,60 @@ + diff --git a/frontend/src/app/feature/transaction-history/transaction-history.component.ts b/frontend/src/app/feature/transaction-history/transaction-history.component.ts new file mode 100644 index 0000000..0bef06d --- /dev/null +++ b/frontend/src/app/feature/transaction-history/transaction-history.component.ts @@ -0,0 +1,54 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + inject, + Input, + Output, +} from '@angular/core'; +import { TransactionService } from '@service/transaction.service'; +import { Observable } from 'rxjs'; +import { AsyncPipe, CurrencyPipe, DatePipe, NgForOf, NgIf } from '@angular/common'; +import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component'; +import { TransactionData } from '../../model/TransactionData'; + +const PER_PAGE = 5; + +@Component({ + standalone: true, + selector: 'app-transaction-history', + imports: [NgForOf, AsyncPipe, CurrencyPipe, DatePipe, AnimatedNumberComponent, NgIf], + templateUrl: './transaction-history.component.html', + styleUrl: './transaction-history.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TransactionHistoryComponent { + @Input() + isOpen = false; + @Output() + closeEventEmitter = new EventEmitter(); + + protected offset = 0; + + private transactionService: TransactionService = inject(TransactionService); + transactionData$: Observable = this.loadTransactions(); + + closeDialog() { + this.isOpen = false; + this.closeEventEmitter.emit(); + } + + forward() { + this.offset++; + this.transactionData$ = this.loadTransactions(); + } + + back() { + this.offset--; + this.transactionData$ = this.loadTransactions(); + } + + loadTransactions() { + return this.transactionService.getUsersTransactions(PER_PAGE, this.offset * PER_PAGE); + } +} diff --git a/frontend/src/app/model/Transaction.ts b/frontend/src/app/model/Transaction.ts index 21277b4..d3bccf1 100644 --- a/frontend/src/app/model/Transaction.ts +++ b/frontend/src/app/model/Transaction.ts @@ -1,6 +1,5 @@ export interface Transaction { - id: string; - type: string; + status: string; amount: number; - date: string; + createdAt: string; } diff --git a/frontend/src/app/model/TransactionData.ts b/frontend/src/app/model/TransactionData.ts new file mode 100644 index 0000000..2da39bb --- /dev/null +++ b/frontend/src/app/model/TransactionData.ts @@ -0,0 +1,6 @@ +import { Transaction } from './Transaction'; + +export interface TransactionData { + transactions: Transaction[]; + hasMore: boolean; +} diff --git a/frontend/src/app/service/transaction.service.ts b/frontend/src/app/service/transaction.service.ts new file mode 100644 index 0000000..55aba0f --- /dev/null +++ b/frontend/src/app/service/transaction.service.ts @@ -0,0 +1,24 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { TransactionData } from '../model/TransactionData'; + +@Injectable({ + providedIn: 'root', +}) +export class TransactionService { + private http: HttpClient = inject(HttpClient); + + public getUsersTransactions(limit: number | null = null, offset: number | null = null) { + const baseUrl = new URL(`${window.location.origin}/backend/user/transactions`); + + if (limit !== null) { + baseUrl.searchParams.append('limit', limit.toString()); + } + + if (offset !== null) { + baseUrl.searchParams.append('offset', offset.toString()); + } + + return this.http.get(`${baseUrl}`); + } +} diff --git a/frontend/src/app/shared/components/confirmation/confirmation.component.ts b/frontend/src/app/shared/components/confirmation/confirmation.component.ts index 8bc884a..9ce91ba 100644 --- a/frontend/src/app/shared/components/confirmation/confirmation.component.ts +++ b/frontend/src/app/shared/components/confirmation/confirmation.component.ts @@ -1,12 +1,12 @@ import { + AfterViewInit, Component, ElementRef, EventEmitter, Input, + OnDestroy, Output, ViewChild, - AfterViewInit, - OnDestroy, } from '@angular/core'; import { ModalAnimationService } from '@shared/services/modal-animation.service'; import gsap from 'gsap'; diff --git a/frontend/src/app/shared/components/footer/footer.component.ts b/frontend/src/app/shared/components/footer/footer.component.ts index 1c3b309..2cb56e1 100644 --- a/frontend/src/app/shared/components/footer/footer.component.ts +++ b/frontend/src/app/shared/components/footer/footer.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { faMoneyBillTransfer, faCreditCard, faWallet } from '@fortawesome/free-solid-svg-icons'; -import { faPaypal, faGooglePay, faApplePay } from '@fortawesome/free-brands-svg-icons'; +import { faCreditCard, faMoneyBillTransfer, faWallet } from '@fortawesome/free-solid-svg-icons'; +import { faApplePay, faGooglePay, faPaypal } from '@fortawesome/free-brands-svg-icons'; @Component({ selector: 'app-footer', diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts index 6bbeeb9..6cc6f56 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.ts +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, inject, - OnInit, OnDestroy, + OnInit, signal, } from '@angular/core'; import { RouterModule } from '@angular/router';