Compare commits

..

No commits in common. "main" and "v1.70.10" have entirely different histories.

106 changed files with 389 additions and 5390 deletions

View file

@ -12,9 +12,3 @@ ci:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- ".gitea/**" - ".gitea/**"
docs:
- changed-files:
- any-glob-to-any-file:
- "projektdokumentation/**"

View file

@ -165,46 +165,6 @@ jobs:
cd frontend cd frontend
bun run lint bun run lint
playwright:
runs-on: ubuntu-latest
name: Playwright
needs: changed_files
container:
image: git.kjan.de/actions/runner-casino-playwright:latest
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: "temurin" # See 'Supported distributions' for available options
java-version: "23"
- name: Install bun
uses: oven-sh/setup-bun@v2
- uses: actions/cache@v4
working-directory: ./frontend
with:
path: |
frontend/node_modules/
key: ${{ runner.os }}-bun-
restore-keys: |
${{ runner.os }}-bun-
- run: bun add -g concurrently
- name: Install dependencies
run: |
cd frontend
bun install
- uses: actions/setup-node@v4
with:
node-version: 22.12
working-directory: ./frontend
- name: Run Playwright tests
env:
CI: true
SPRING_PROFILES_ACTIVE: inmemory
working-directory: ./frontend
run: bash -c "source $HOME/.cargo/env && bunx playwright test"
oxlint: oxlint:
runs-on: docker runs-on: docker
name: oxlint name: oxlint

View file

@ -1,29 +0,0 @@
name: Build docs
on:
pull_request:
push:
branches: [main]
jobs:
build-docs:
runs-on: ubuntu-latest
container:
image: git.kjan.de/actions/runner-latex:latest
env:
# Edit here with the names of your latex file and directory (can use ".")
DIR: projektdokumentation
FILE: Projektdokumentation.tex
steps:
- name: Checkout
uses: actions/checkout@v4
- name: LaTeX compile
working-directory: ${{ env.DIR }}
run: latexmk -pdf ${{ env.FILE }}
- name: Upload artifacts
uses: https://git.kjan.de/actions/upload-artifact@v3 # Do not upgrade
with:
name: Doku
path: projektdokumentation/Projektdokumentation.pdf

View file

@ -50,12 +50,11 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.0") implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.0")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.5.0") implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.5.0")
runtimeOnly("org.postgresql:postgresql") runtimeOnly("org.postgresql:postgresql")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8")
implementation("io.jsonwebtoken:jjwt-api:0.12.6") implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6") runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.springframework.boot:spring-boot-starter-mail")
runtimeOnly("com.h2database:h2")
} }
tasks.withType<Test> { tasks.withType<Test> {

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View file

@ -74,7 +74,8 @@ public class CasinoApplication {
rewardRepository.saveAll(Arrays.asList( rewardRepository.saveAll(Arrays.asList(
commonReward, rareReward, epicReward, commonReward, rareReward, epicReward,
premiumCommon, premiumRare, legendaryReward)); premiumCommon, premiumRare, legendaryReward
));
basicLootBox.getRewards().add(commonReward); basicLootBox.getRewards().add(commonReward);
basicLootBox.getRewards().add(premiumRare); basicLootBox.getRewards().add(premiumRare);

View file

@ -25,7 +25,7 @@ public class RewardEntity {
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@Column(precision = 19, scale = 2, name = "rewardValue") @Column(precision = 19, scale = 2)
private BigDecimal value; private BigDecimal value;
@Column(precision = 5, scale = 2) @Column(precision = 5, scale = 2)

View file

@ -1,8 +1,5 @@
package de.szut.casino.security.oauth2.github; package de.szut.casino.security.oauth2.github;
import de.szut.casino.deposit.TransactionEntity;
import de.szut.casino.deposit.TransactionRepository;
import de.szut.casino.deposit.TransactionStatus;
import de.szut.casino.security.dto.AuthResponseDto; import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.jwt.JwtUtils; import de.szut.casino.security.jwt.JwtUtils;
import de.szut.casino.user.AuthProvider; import de.szut.casino.user.AuthProvider;
@ -33,14 +30,12 @@ public class GitHubService {
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final UserRepository userRepository; private final UserRepository userRepository;
private final TransactionRepository transactionRepository;
private final JwtUtils jwtUtils; private final JwtUtils jwtUtils;
private final PasswordEncoder oauth2PasswordEncoder; private final PasswordEncoder oauth2PasswordEncoder;
public GitHubService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) { public GitHubService(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
this.authenticationManager = authenticationManager; this.authenticationManager = authenticationManager;
this.userRepository = userRepository; this.userRepository = userRepository;
this.transactionRepository = transactionRepository;
this.jwtUtils = jwtUtils; this.jwtUtils = jwtUtils;
this.oauth2PasswordEncoder = oauth2PasswordEncoder; this.oauth2PasswordEncoder = oauth2PasswordEncoder;
} }
@ -144,20 +139,15 @@ public class GitHubService {
user.setProvider(AuthProvider.GITHUB); user.setProvider(AuthProvider.GITHUB);
user.setProviderId(githubId); user.setProviderId(githubId);
user.setEmailVerified(true); user.setEmailVerified(true);
user.setBalance(new BigDecimal("100.00"));
user.setBalance(new BigDecimal("1000.00"));
} }
} }
String randomPassword = UUID.randomUUID().toString(); String randomPassword = UUID.randomUUID().toString();
user.setPassword(oauth2PasswordEncoder.encode(randomPassword)); user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
TransactionEntity transaction = new TransactionEntity();
transaction.setAmount(100L);
transaction.setUser(user);
transaction.setSessionId("signup_bonus");
transaction.setStatus(TransactionStatus.SUCCEEDED);
userRepository.save(user); userRepository.save(user);
transactionRepository.save(transaction);
Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword)); Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword));

View file

@ -1,8 +1,5 @@
package de.szut.casino.security.oauth2.google; package de.szut.casino.security.oauth2.google;
import de.szut.casino.deposit.TransactionEntity;
import de.szut.casino.deposit.TransactionRepository;
import de.szut.casino.deposit.TransactionStatus;
import de.szut.casino.security.dto.AuthResponseDto; import de.szut.casino.security.dto.AuthResponseDto;
import de.szut.casino.security.jwt.JwtUtils; import de.szut.casino.security.jwt.JwtUtils;
import de.szut.casino.user.AuthProvider; import de.szut.casino.user.AuthProvider;
@ -50,14 +47,12 @@ public class GoogleService {
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final UserRepository userRepository; private final UserRepository userRepository;
private final TransactionRepository transactionRepository;
private final JwtUtils jwtUtils; private final JwtUtils jwtUtils;
private final PasswordEncoder oauth2PasswordEncoder; private final PasswordEncoder oauth2PasswordEncoder;
public GoogleService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) { public GoogleService(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
this.authenticationManager = authenticationManager; this.authenticationManager = authenticationManager;
this.userRepository = userRepository; this.userRepository = userRepository;
this.transactionRepository = transactionRepository;
this.jwtUtils = jwtUtils; this.jwtUtils = jwtUtils;
this.oauth2PasswordEncoder = oauth2PasswordEncoder; this.oauth2PasswordEncoder = oauth2PasswordEncoder;
} }
@ -151,14 +146,8 @@ public class GoogleService {
String randomPassword = UUID.randomUUID().toString(); String randomPassword = UUID.randomUUID().toString();
user.setPassword(oauth2PasswordEncoder.encode(randomPassword)); user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
TransactionEntity transaction = new TransactionEntity();
transaction.setAmount(100L);
transaction.setUser(user);
transaction.setSessionId("signup_bonus");
transaction.setStatus(TransactionStatus.SUCCEEDED);
userRepository.save(user); userRepository.save(user);
transactionRepository.save(transaction);
Authentication authentication = authenticationManager.authenticate( Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword) new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword)

View file

@ -1,7 +1,5 @@
package de.szut.casino.user; package de.szut.casino.user;
import de.szut.casino.deposit.TransactionEntity;
import de.szut.casino.deposit.TransactionStatus;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
import de.szut.casino.user.dto.CreateUserDto; import de.szut.casino.user.dto.CreateUserDto;
import jakarta.persistence.EntityExistsException; import jakarta.persistence.EntityExistsException;
@ -40,12 +38,6 @@ public class UserService {
RandomStringUtils.randomAlphanumeric(64) RandomStringUtils.randomAlphanumeric(64)
); );
TransactionEntity transaction = new TransactionEntity();
transaction.setAmount(100L);
transaction.setUser(user);
transaction.setSessionId("signup_bonus");
transaction.setStatus(TransactionStatus.SUCCEEDED);
return userRepository.save(user); return userRepository.save(user);
} }

View file

@ -1,58 +0,0 @@
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
server.port=${HTTP_PORT:8080}
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15}
app.frontend-host=${FE_URL:http://localhost:4200}
app.mail.authentication=${MAIL_AUTHENTICATION:false}
app.mail.host=${MAIL_HOST:localhost}
app.mail.port=${MAIL_PORT:1025}
app.mail.username=${MAIL_USER:null}
app.mail.password=${MAIL_PASS:null}
app.mail.from-address=${MAIL_FROM:casino@localhost}
app.mail.protocol=${MAIL_PROTOCOL:smtp}
spring.application.name=casino
# JWT Configuration
jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437}
jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000}
# Logging
logging.level.org.springframework.security=DEBUG
# Swagger
springdoc.swagger-ui.path=swagger
springdoc.swagger-ui.try-it-out-enabled=true
# GitHub OAuth2 Configuration
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID:Ov23lingzZsPn1wwACoK}
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET:4b327fb3b1ab67584a03bcb9d53fa6439fbccad7}
spring.security.oauth2.client.registration.github.redirect-uri=${app.frontend-host}/oauth2/callback/github
spring.security.oauth2.client.registration.github.scope=user:email,read:user
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
spring.security.oauth2.client.provider.github.user-name-attribute=login
# OAuth Success and Failure URLs
app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback
# Google OAuth2 Configuration
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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}
spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google
spring.security.oauth2.client.registration.google.scope=email,profile
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
spring.security.oauth2.client.provider.google.user-name-attribute=sub

3
frontend/.gitignore vendored
View file

@ -29,9 +29,6 @@ yarn-error.log
.history/* .history/*
# Miscellaneous # Miscellaneous
/.claude
/test-results
/playwright-report
/.angular/cache /.angular/cache
.sass-cache/ .sass-cache/
/connect.lock /connect.lock

View file

@ -21,8 +21,7 @@
{ {
"glob": "**/*", "glob": "**/*",
"input": "public" "input": "public"
}, }
"src/assets"
], ],
"styles": [ "styles": [
"src/styles.css" "src/styles.css"

View file

@ -32,11 +32,10 @@
"@angular-devkit/build-angular": "^20.0.0", "@angular-devkit/build-angular": "^20.0.0",
"@angular/cli": "^20.0.0", "@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0",
"@playwright/test": "^1.52.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"angular-eslint": "20.0.0", "angular-eslint": "19.7.1",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"jasmine-core": "~5.8.0", "jasmine-core": "~5.7.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
@ -44,7 +43,7 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"typescript-eslint": "8.34.0", "typescript-eslint": "8.33.1",
}, },
}, },
}, },
@ -63,19 +62,19 @@
"@angular-devkit/schematics": ["@angular-devkit/schematics@20.0.0", "", { "dependencies": { "@angular-devkit/core": "20.0.0", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "8.2.0", "rxjs": "7.8.2" } }, "sha512-35WbWP8ARnaqVjOzy7IOyWsY/jeyUqfVj4KgHG2O4fHAhIhaBqhP8dDDP+SwM+bToIqklg0fzHUUhFTRxzzyoQ=="], "@angular-devkit/schematics": ["@angular-devkit/schematics@20.0.0", "", { "dependencies": { "@angular-devkit/core": "20.0.0", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "8.2.0", "rxjs": "7.8.2" } }, "sha512-35WbWP8ARnaqVjOzy7IOyWsY/jeyUqfVj4KgHG2O4fHAhIhaBqhP8dDDP+SwM+bToIqklg0fzHUUhFTRxzzyoQ=="],
"@angular-eslint/builder": ["@angular-eslint/builder@20.0.0", "", { "dependencies": { "@angular-devkit/architect": ">= 0.2000.0 < 0.2100.0", "@angular-devkit/core": ">= 20.0.0 < 21.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-9jS3VvY+K+EHw9pofsdwKxDirKuTuRBnjMZdaKoUfLoYy5eS1XGJBXoMdaQiM+mSlTv113+L0SK4U565xiBLHQ=="], "@angular-eslint/builder": ["@angular-eslint/builder@19.7.1", "", { "dependencies": { "@angular-devkit/architect": ">= 0.1900.0 < 0.2000.0", "@angular-devkit/core": ">= 19.0.0 < 20.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-11zk4JngV4KgnDNtaiYzx0EhwMFdSbZjSF+CMHN4yB90e8vukhQDg/L8PEdSUEnkBKMymK9xN1JLIE+n6ESPaQ=="],
"@angular-eslint/bundled-angular-compiler": ["@angular-eslint/bundled-angular-compiler@20.0.0", "", {}, "sha512-mDXMQd08s11q9fC6Ps3ffZmvXop9eLuAAXexofHhA7uuoQAoUWS2zoOSNTWtDR6oxMcqEeMnALCjjFeJVBSVmg=="], "@angular-eslint/bundled-angular-compiler": ["@angular-eslint/bundled-angular-compiler@19.7.1", "", {}, "sha512-L2xpEbyT3EJQ/dTmPntBu6MBeEG+3SKARJ9xuA4nnm6n7nn9EARmUmaGlBQZ2DFMOFeKcqR802gdPi8vCADXRA=="],
"@angular-eslint/eslint-plugin": ["@angular-eslint/eslint-plugin@20.0.0", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.0.0", "@angular-eslint/utils": "20.0.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-xzaLj2yEn43DH0bE9Gw3GrmC+jivIS5/Hbh3bDj3ctw3mUUrD8hrS7kBo1neZ0gnoVLoo/mwIldG+xs5NDY66A=="], "@angular-eslint/eslint-plugin": ["@angular-eslint/eslint-plugin@19.7.1", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "19.7.1", "@angular-eslint/utils": "19.7.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-VdNES8AxOHpSKTBfEkUtlvm/EV4BlpvWvMK2T0Eu2y2oHjxtBB6V4vYZR0PlSWS9IrE1MPfuH1htHPovBxiEdw=="],
"@angular-eslint/eslint-plugin-template": ["@angular-eslint/eslint-plugin-template@20.0.0", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.0.0", "@angular-eslint/utils": "20.0.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { "@angular-eslint/template-parser": "20.0.0", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-QoGgrawU5JFcaj0TjXHKC6fiZkxBeGVRj/TWJtTo/x+c5TVoV5k9pI7Uxdmo9kr4SkPXmt80ZklvExSA510gyw=="], "@angular-eslint/eslint-plugin-template": ["@angular-eslint/eslint-plugin-template@19.7.1", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "19.7.1", "@angular-eslint/utils": "19.7.1", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { "@angular-eslint/template-parser": "19.7.1", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-VQMlNF29Xbff6KMkUBWHbUyl0tIos1yI//q+ha+DCNYD7l41uoKnRIJ9c5vehXdgsqHWm4XBCVB3HKKMbrabrg=="],
"@angular-eslint/schematics": ["@angular-eslint/schematics@20.0.0", "", { "dependencies": { "@angular-devkit/core": ">= 20.0.0 < 21.0.0", "@angular-devkit/schematics": ">= 20.0.0 < 21.0.0", "@angular-eslint/eslint-plugin": "20.0.0", "@angular-eslint/eslint-plugin-template": "20.0.0", "ignore": "7.0.5", "semver": "7.7.2", "strip-json-comments": "3.1.1" } }, "sha512-VL3Sb6Df+iiUSPaQG8NxMPLx0dFRtRGSzsfe6CWYW7FUFP5dYEjpB63gKSAiIBLjPgnG6PMAzrRtfN4nDaTM+g=="], "@angular-eslint/schematics": ["@angular-eslint/schematics@19.7.1", "", { "dependencies": { "@angular-devkit/core": ">= 19.0.0 < 20.0.0", "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", "@angular-eslint/eslint-plugin": "19.7.1", "@angular-eslint/eslint-plugin-template": "19.7.1", "ignore": "7.0.5", "semver": "7.7.2", "strip-json-comments": "3.1.1" } }, "sha512-IlfzqpAx+491G0yCa+qaqsAbPWZxRywfNc4SGjIGgze8Nal0PbTy8VLZgAJ7/u/rEUowSvt3oKytb9rGS5u9kw=="],
"@angular-eslint/template-parser": ["@angular-eslint/template-parser@20.0.0", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.0.0", "eslint-scope": "^8.0.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-5y9hxH/z+9rIOJp1FwRBSgJ6xt8/pgRfBF+eEIPyIHKl5mV0cVzlQiD7j1LMYTcxJZLHAoryomvSBDpmbtAlWg=="], "@angular-eslint/template-parser": ["@angular-eslint/template-parser@19.7.1", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "19.7.1", "eslint-scope": "^8.0.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-SEuOjQJBTOOJ2DK0Wfiu1nO1F+fkXVehIk8xabEKZBY+t2mqbAiwBeWZsCSm4xx4BE689FwCHjVEF2c0yLvq1A=="],
"@angular-eslint/utils": ["@angular-eslint/utils@20.0.0", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.0.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-3wsx0iX5f/IQgcTwXIzQq2VPHSjYXJasKNSfgMyKXn4MJGljaSNj+A0ao/5zjnwWVpL0vK5PQsk7EIuMcgAdrg=="], "@angular-eslint/utils": ["@angular-eslint/utils@19.7.1", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "19.7.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "sha512-iNfAwnQFBwNwh64BnTIdypFnBwyjjxtdEtAwNxM0u5ApX2MAu+koHdwGiXvyeJPq+g7stXwVpuvaAnHlxTzcig=="],
"@angular/animations": ["@angular/animations@20.0.0", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "20.0.0", "@angular/core": "20.0.0" } }, "sha512-yU4hUH6AheY0dnMSaLRMfgnXhg/JUSUvrhE+lHzIiSKdEf0lyo1Ri6bkPD1CbamxZ94BqhRNCApvbvTbibGICQ=="], "@angular/animations": ["@angular/animations@20.0.0", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "20.0.0", "@angular/core": "20.0.0" } }, "sha512-yU4hUH6AheY0dnMSaLRMfgnXhg/JUSUvrhE+lHzIiSKdEf0lyo1Ri6bkPD1CbamxZ94BqhRNCApvbvTbibGICQ=="],
@ -543,8 +542,6 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@playwright/test": ["@playwright/test@1.52.0", "", { "dependencies": { "playwright": "1.52.0" }, "bin": { "playwright": "cli.js" } }, "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.2", "", { "os": "android", "cpu": "arm" }, "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.2", "", { "os": "android", "cpu": "arm" }, "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.2", "", { "os": "android", "cpu": "arm64" }, "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.2", "", { "os": "android", "cpu": "arm64" }, "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw=="],
@ -687,9 +684,9 @@
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.34.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/type-utils": "8.34.0", "@typescript-eslint/utils": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/type-utils": "8.33.1", "@typescript-eslint/utils": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.34.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.1", "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.1", "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw=="],
@ -697,7 +694,7 @@
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.34.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.33.1", "", {}, "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.33.1", "", {}, "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg=="],
@ -705,7 +702,7 @@
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="],
"@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.0.0", "", { "peerDependencies": { "vite": "^6.0.0" } }, "sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA=="], "@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.0.0", "", { "peerDependencies": { "vite": "^6.0.0" } }, "sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA=="],
@ -763,7 +760,7 @@
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"angular-eslint": ["angular-eslint@20.0.0", "", { "dependencies": { "@angular-devkit/core": ">= 20.0.0 < 21.0.0", "@angular-devkit/schematics": ">= 20.0.0 < 21.0.0", "@angular-eslint/builder": "20.0.0", "@angular-eslint/eslint-plugin": "20.0.0", "@angular-eslint/eslint-plugin-template": "20.0.0", "@angular-eslint/schematics": "20.0.0", "@angular-eslint/template-parser": "20.0.0", "@typescript-eslint/types": "^8.0.0", "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*", "typescript-eslint": "^8.0.0" } }, "sha512-9wCkzR+oxMKDXktFItI10dFaX4qCuz9SgClXdh/ZHmCANHK/RtPnXnD+gROPvhNN1M6BAJKialjIrs88orz97A=="], "angular-eslint": ["angular-eslint@19.7.1", "", { "dependencies": { "@angular-devkit/core": ">= 19.0.0 < 20.0.0", "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", "@angular-eslint/builder": "19.7.1", "@angular-eslint/eslint-plugin": "19.7.1", "@angular-eslint/eslint-plugin-template": "19.7.1", "@angular-eslint/schematics": "19.7.1", "@angular-eslint/template-parser": "19.7.1", "@typescript-eslint/types": "^8.0.0", "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "*", "typescript-eslint": "^8.0.0" } }, "sha512-xSjDf3Tdc7gA99Uk4sOfP/3I2jvKlCvuFnqWxw2mzByVNAgT9QUTY51+Z0jFT8JOnsN+UwE3fDPOHuSu183Mew=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
@ -797,6 +794,8 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
"batch": ["batch@0.6.1", "", {}, "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw=="], "batch": ["batch@0.6.1", "", {}, "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw=="],
@ -807,6 +806,8 @@
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
"bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
@ -819,6 +820,8 @@
"browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@ -855,6 +858,8 @@
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
"clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@ -921,6 +926,8 @@
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
@ -1075,7 +1082,7 @@
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@ -1145,6 +1152,8 @@
"icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="], "icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"ignore-walk": ["ignore-walk@7.0.0", "", { "dependencies": { "minimatch": "^9.0.0" } }, "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ=="], "ignore-walk": ["ignore-walk@7.0.0", "", { "dependencies": { "minimatch": "^9.0.0" } }, "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ=="],
@ -1221,7 +1230,7 @@
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jasmine-core": ["jasmine-core@5.8.0", "", {}, "sha512-Q9dqmpUAfptwyueW3+HqBOkSuYd9I/clZSSfN97wXE/Nr2ROFNCwIBEC1F6kb3QXS9Fcz0LjFYSDQT+BiwjuhA=="], "jasmine-core": ["jasmine-core@5.7.1", "", {}, "sha512-QnurrtpKsPoixxG2R3d1xP0St/2kcX5oTZyDyQJMY+Vzi/HUlu1kGm+2V8Tz+9lV991leB1l0xcsyz40s9xOOw=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
@ -1353,6 +1362,8 @@
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"mini-css-extract-plugin": ["mini-css-extract-plugin@2.9.2", "", { "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" }, "peerDependencies": { "webpack": "^5.0.0" } }, "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w=="], "mini-css-extract-plugin": ["mini-css-extract-plugin@2.9.2", "", { "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" }, "peerDependencies": { "webpack": "^5.0.0" } }, "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w=="],
@ -1503,10 +1514,6 @@
"piscina": ["piscina@5.0.0", "", { "optionalDependencies": { "@napi-rs/nice": "^1.0.1" } }, "sha512-R+arufwL7sZvGjAhSMK3TfH55YdGOqhpKXkcwQJr432AAnJX/xxX19PA4QisrmJ+BTTfZVggaz6HexbkQq1l1Q=="], "piscina": ["piscina@5.0.0", "", { "optionalDependencies": { "@napi-rs/nice": "^1.0.1" } }, "sha512-R+arufwL7sZvGjAhSMK3TfH55YdGOqhpKXkcwQJr432AAnJX/xxX19PA4QisrmJ+BTTfZVggaz6HexbkQq1l1Q=="],
"playwright": ["playwright@1.52.0", "", { "dependencies": { "playwright-core": "1.52.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw=="],
"playwright-core": ["playwright-core@1.52.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg=="],
"postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="], "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="],
"postcss-loader": ["postcss-loader@8.1.1", "", { "dependencies": { "cosmiconfig": "^9.0.0", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ=="], "postcss-loader": ["postcss-loader@8.1.1", "", { "dependencies": { "cosmiconfig": "^9.0.0", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ=="],
@ -1755,7 +1762,7 @@
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-eslint": ["typescript-eslint@8.34.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.34.0", "@typescript-eslint/parser": "8.34.0", "@typescript-eslint/utils": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ=="], "typescript-eslint": ["typescript-eslint@8.33.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.1", "@typescript-eslint/parser": "8.33.1", "@typescript-eslint/utils": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A=="],
"ua-parser-js": ["ua-parser-js@0.7.40", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ=="], "ua-parser-js": ["ua-parser-js@0.7.40", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ=="],
@ -1801,6 +1808,8 @@
"wbuf": ["wbuf@1.7.3", "", { "dependencies": { "minimalistic-assert": "^1.0.0" } }, "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA=="], "wbuf": ["wbuf@1.7.3", "", { "dependencies": { "minimalistic-assert": "^1.0.0" } }, "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA=="],
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
"weak-lru-cache": ["weak-lru-cache@1.2.2", "", {}, "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw=="], "weak-lru-cache": ["weak-lru-cache@1.2.2", "", {}, "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw=="],
"webpack": ["webpack@5.99.8", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ=="], "webpack": ["webpack@5.99.8", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ=="],
@ -1849,6 +1858,14 @@
"@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], "@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
"@angular-eslint/builder/@angular-devkit/architect": ["@angular-devkit/architect@0.1902.14", "", { "dependencies": { "@angular-devkit/core": "19.2.14", "rxjs": "7.8.1" } }, "sha512-rgMkqOrxedzqLZ8w59T/0YrpWt7LDmGwt+ZhNHE7cn27jZ876yGC2Bhcn58YZh2+R03WEJ9q0ePblaBYz03SMw=="],
"@angular-eslint/builder/@angular-devkit/core": ["@angular-devkit/core@19.2.14", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-aaPEnRNIBoYT4XrrYcZlHadX8vFDTUR+4wUgcmr0cNDLeWzWtoPFeVq8TQD6kFDeqovSx/UVEblGgg/28WvHyg=="],
"@angular-eslint/schematics/@angular-devkit/core": ["@angular-devkit/core@19.2.14", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-aaPEnRNIBoYT4XrrYcZlHadX8vFDTUR+4wUgcmr0cNDLeWzWtoPFeVq8TQD6kFDeqovSx/UVEblGgg/28WvHyg=="],
"@angular-eslint/schematics/@angular-devkit/schematics": ["@angular-devkit/schematics@19.2.14", "", { "dependencies": { "@angular-devkit/core": "19.2.14", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" } }, "sha512-s89/MWXHy8+GP/cRfFbSECIG3FQQQwNVv44OOmghPVgKQgQ+EoE/zygL2hqKYTUPoPaS/IhNXdXjSE5pS9yLeg=="],
"@angular-eslint/schematics/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@angular-eslint/schematics/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@babel/core/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="], "@babel/core/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="],
@ -1947,34 +1964,18 @@
"@tufjs/models/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@tufjs/models/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"adjust-sourcemap-loader/loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="], "adjust-sourcemap-loader/loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
"angular-eslint/@angular-devkit/core": ["@angular-devkit/core@19.2.14", "", { "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" }, "peerDependencies": { "chokidar": "^4.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-aaPEnRNIBoYT4XrrYcZlHadX8vFDTUR+4wUgcmr0cNDLeWzWtoPFeVq8TQD6kFDeqovSx/UVEblGgg/28WvHyg=="],
"angular-eslint/@angular-devkit/schematics": ["@angular-devkit/schematics@19.2.14", "", { "dependencies": { "@angular-devkit/core": "19.2.14", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" } }, "sha512-s89/MWXHy8+GP/cRfFbSECIG3FQQQwNVv44OOmghPVgKQgQ+EoE/zygL2hqKYTUPoPaS/IhNXdXjSE5pS9yLeg=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@ -2101,8 +2102,6 @@
"resolve-url-loader/loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="], "resolve-url-loader/loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
"rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@ -2141,10 +2140,6 @@
"tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="],
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], "vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
@ -2163,6 +2158,20 @@
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@angular-eslint/builder/@angular-devkit/architect/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"@angular-eslint/builder/@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"@angular-eslint/builder/@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
"@angular-eslint/schematics/@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"@angular-eslint/schematics/@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
"@angular-eslint/schematics/@angular-devkit/schematics/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@ -2207,32 +2216,16 @@
"@tufjs/models/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@tufjs/models/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"angular-eslint/@angular-devkit/core/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"angular-eslint/@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
"angular-eslint/@angular-devkit/schematics/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
"angular-eslint/@angular-devkit/schematics/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@ -2273,8 +2266,6 @@
"karma-coverage/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "karma-coverage/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"karma/chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"karma/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "karma/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"karma/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "karma/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
@ -2315,14 +2306,6 @@
"tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="],
"webpack-dev-server/chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"webpack-dev-server/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "webpack-dev-server/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"webpack-dev-server/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "webpack-dev-server/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
@ -2337,6 +2320,16 @@
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@ -2353,15 +2346,15 @@
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="], "angular-eslint/@angular-devkit/schematics/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="], "angular-eslint/@angular-devkit/schematics/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "angular-eslint/@angular-devkit/schematics/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "angular-eslint/@angular-devkit/schematics/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "angular-eslint/@angular-devkit/schematics/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
@ -2379,24 +2372,30 @@
"karma/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "karma/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"webpack-dev-server/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "webpack-dev-server/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "angular-eslint/@angular-devkit/schematics/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
"angular-eslint/@angular-devkit/schematics/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"karma/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "karma/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"karma/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "karma/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@angular-eslint/schematics/@angular-devkit/schematics/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"@angular-eslint/schematics/@angular-devkit/schematics/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"angular-eslint/@angular-devkit/schematics/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"angular-eslint/@angular-devkit/schematics/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
} }
} }

View file

@ -1,8 +0,0 @@
import { test, expect } from '@playwright/test';
test('backend works', async ({ page }) => {
await page.goto('http://localhost:8080/health');
const response = await page.textContent('body');
expect(response).toBeTruthy();
expect(page.getByText('{"status":"UP"}')).toBeVisible();
});

View file

@ -1,36 +0,0 @@
import { test, expect } from '@playwright/test';
test('home page loads correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Casino/);
await expect(page.getByRole('heading', { name: 'Willkommensbonus' })).toBeVisible();
await expect(page.getByText('von bis zu €')).toBeVisible();
});
test('registration popup should open and close', async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
await expect(page.getByText('Konto erstellenE-')).toBeVisible();
await page.getByRole('button', { name: 'Dialog schließen' }).click();
await expect(page.getByText('Konto erstellenE-')).not.toBeVisible();
});
test('registration should work', async ({ page }) => {
await page.goto('/');
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
await page.getByRole('textbox', { name: 'E-Mail' }).fill('test@kjan.email');
await page.getByRole('textbox', { name: 'Benutzername' }).fill('test-playwright');
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
await page.locator('form').getByRole('button', { name: 'Registrieren' }).click();
await page.getByRole('button', { name: 'Dialog schließen' }).click();
await page.getByRole('navigation').getByRole('button', { name: 'Anmelden' }).click();
await page.getByRole('textbox', { name: 'Benutzername oder E-Mail' }).fill('test@kjan.email');
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
await page.locator('form').getByRole('button', { name: 'Anmelden' }).click();
await expect(page.getByText('Email not verified')).toBeVisible();
});

View file

@ -39,14 +39,13 @@
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "^20.0.0",
"@angular-devkit/build-angular": "^20.0.0", "@angular-devkit/build-angular": "^20.0.0",
"@angular/cli": "^20.0.0", "@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@playwright/test": "^1.52.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"angular-eslint": "20.0.0", "angular-eslint": "19.7.1",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"jasmine-core": "~5.8.0", "jasmine-core": "~5.7.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
@ -54,6 +53,6 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"typescript-eslint": "8.34.0" "typescript-eslint": "8.33.1"
} }
} }

View file

@ -1,49 +0,0 @@
// playwright.config.ts (or .js)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
// This baseURL is for your frontend tests.
// Tests hitting the backend directly will use absolute URLs.
baseURL: 'http://localhost:4200',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
webServer: {
command:
'cd .. && conc -n "frontend,backend" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun"',
// **IMPORTANT CHANGE HERE:**
// Point to your backend's health check endpoint.
// If your Spring Boot app uses Actuator, it might be /actuator/health
// Verify the correct health endpoint for your backend.
url: 'http://localhost:8080/health', // Or "http://localhost:8080/actuator/health"
reuseExistingServer: !process.env.CI,
// **INCREASE TIMEOUT SIGNIFICANTLY**
// Gradle + Spring Boot can take a while, especially on first run or in CI.
// Adjust as needed, e.g., 3-5 minutes.
timeout: 300 * 1000, // 300 seconds = 5 minutes
stdout: 'pipe', // Good for capturing logs in CI reports
stderr: 'pipe',
// Optional: If your server needs specific environment variables
// env: {
// SPRING_PROFILES_ACTIVE: 'test', // Example for Spring Boot
// },
},
});

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Output, signal, inject } from '@angular/core'; import { Component, EventEmitter, Output, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { LoginRequest } from '../../../model/auth/LoginRequest'; import { LoginRequest } from '../../../model/auth/LoginRequest';
@ -20,11 +20,11 @@ export class LoginComponent {
@Output() closeDialog = new EventEmitter<void>(); @Output() closeDialog = new EventEmitter<void>();
@Output() forgotPassword = new EventEmitter<void>(); @Output() forgotPassword = new EventEmitter<void>();
private fb = inject(FormBuilder); constructor(
private authService = inject(AuthService); private fb: FormBuilder,
private router = inject(Router); private authService: AuthService,
private router: Router
constructor() { ) {
this.loginForm = this.fb.group({ this.loginForm = this.fb.group({
usernameOrEmail: ['', [Validators.required]], usernameOrEmail: ['', [Validators.required]],
password: ['', [Validators.required]], password: ['', [Validators.required]],

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Output, signal, OnInit, inject } from '@angular/core'; import { Component, EventEmitter, Output, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@ -22,12 +22,12 @@ export default class RecoverPasswordComponent implements OnInit {
@Output() closeDialog = new EventEmitter<void>(); @Output() closeDialog = new EventEmitter<void>();
@Output() switchToLogin = new EventEmitter<void>(); @Output() switchToLogin = new EventEmitter<void>();
private fb = inject(FormBuilder); constructor(
private authService = inject(AuthService); private fb: FormBuilder,
private router = inject(Router); private authService: AuthService,
private route = inject(ActivatedRoute); private router: Router,
private route: ActivatedRoute
constructor() { ) {
this.emailForm = this.fb.group({ this.emailForm = this.fb.group({
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
}); });

View file

@ -1,4 +1,4 @@
import { Component, EventEmitter, Output, signal, inject } from '@angular/core'; import { Component, EventEmitter, Output, signal } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { RegisterRequest } from '../../../model/auth/RegisterRequest'; import { RegisterRequest } from '../../../model/auth/RegisterRequest';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
@ -19,10 +19,10 @@ export class RegisterComponent {
@Output() switchForm = new EventEmitter<void>(); @Output() switchForm = new EventEmitter<void>();
@Output() closeDialog = new EventEmitter<void>(); @Output() closeDialog = new EventEmitter<void>();
private fb = inject(FormBuilder); constructor(
private authService = inject(AuthService); private fb: FormBuilder,
private authService: AuthService
constructor() { ) {
this.registerForm = this.fb.group({ this.registerForm = this.fb.group({
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
username: ['', [Validators.required, Validators.minLength(3)]], username: ['', [Validators.required, Validators.minLength(3)]],

View file

@ -61,9 +61,6 @@ export class AnimatedNumberComponent implements OnChanges, AfterViewInit {
this.countUp = new CountUp(this.numberElement.nativeElement, this.value, { this.countUp = new CountUp(this.numberElement.nativeElement, this.value, {
startVal: this.previousValue, startVal: this.previousValue,
duration: this.duration, duration: this.duration,
decimalPlaces: 2,
useEasing: true,
useGrouping: false,
easingFn: (t, b, c, d) => { easingFn: (t, b, c, d) => {
if (this.ease === 'power1.out') { if (this.ease === 'power1.out') {
return c * (1 - Math.pow(1 - t / d, 1)) + b; return c * (1 - Math.pow(1 - t / d, 1)) + b;

View file

@ -1,11 +1,4 @@
import { import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
SimpleChanges,
inject,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Card } from '@blackjack/models/blackjack.model'; import { Card } from '@blackjack/models/blackjack.model';
import { PlayingCardComponent } from '../playing-card/playing-card.component'; import { PlayingCardComponent } from '../playing-card/playing-card.component';
@ -54,7 +47,7 @@ export class DealerHandComponent implements OnChanges {
private lastCardCount = 0; private lastCardCount = 0;
protected gameControlsService = inject(GameControlsService); constructor(protected gameControlsService: GameControlsService) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes['cards']) { if (changes['cards']) {

View file

@ -1,11 +1,4 @@
import { import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
inject,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { GameState } from '@blackjack/enum/gameState'; import { GameState } from '@blackjack/enum/gameState';
import { Card } from '@blackjack/models/blackjack.model'; import { Card } from '@blackjack/models/blackjack.model';
@ -76,7 +69,7 @@ export class GameControlsComponent {
protected readonly GameState = GameState; protected readonly GameState = GameState;
protected gameControlsService = inject(GameControlsService); constructor(protected gameControlsService: GameControlsService) {}
get canDoubleDown(): boolean { get canDoubleDown(): boolean {
return ( return (

View file

@ -7,7 +7,6 @@ import {
Output, Output,
signal, signal,
SimpleChanges, SimpleChanges,
inject,
} from '@angular/core'; } from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common'; import { CommonModule, CurrencyPipe } from '@angular/common';
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormGroup, ReactiveFormsModule } from '@angular/forms';
@ -122,9 +121,7 @@ export class GameInfoComponent implements OnChanges {
betForm: FormGroup; betForm: FormGroup;
private bettingService = inject(BettingService); constructor(private bettingService: BettingService) {
constructor() {
this.betForm = this.bettingService.createBetForm(); this.betForm = this.bettingService.createBetForm();
} }

View file

@ -1,11 +1,4 @@
import { import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
SimpleChanges,
inject,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from '../playing-card/playing-card.component'; import { PlayingCardComponent } from '../playing-card/playing-card.component';
import { Card } from '@blackjack/models/blackjack.model'; import { Card } from '@blackjack/models/blackjack.model';
@ -56,7 +49,7 @@ export class PlayerHandComponent implements OnChanges {
private lastCardCount = 0; private lastCardCount = 0;
protected gameControlsService = inject(GameControlsService); constructor(protected gameControlsService: GameControlsService) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes['cards']) { if (changes['cards']) {

View file

@ -6,7 +6,6 @@ import {
Input, Input,
OnChanges, OnChanges,
SimpleChanges, SimpleChanges,
inject,
} from '@angular/core'; } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { gsap } from 'gsap'; import { gsap } from 'gsap';
@ -59,7 +58,7 @@ export class PlayingCardComponent implements AfterViewInit, OnChanges {
@Input({ required: true }) hidden!: boolean; @Input({ required: true }) hidden!: boolean;
@Input() isNew = false; @Input() isNew = false;
private elementRef = inject(ElementRef); constructor(private elementRef: ElementRef) {}
get isRedSuit(): boolean { get isRedSuit(): boolean {
return this.suit === 'HEARTS' || this.suit === 'DIAMONDS'; return this.suit === 'HEARTS' || this.suit === 'DIAMONDS';

View file

@ -1,11 +1,11 @@
import { Injectable, inject } from '@angular/core'; import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class BettingService { export class BettingService {
private fb = inject(FormBuilder); constructor(private fb: FormBuilder) {}
createBetForm(): FormGroup { createBetForm(): FormGroup {
return this.fb.group({ return this.fb.group({

View file

@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DiceDto, DiceResult } from './dice.model'; import { DiceDto, DiceResult } from './dice.model';
@ -10,7 +10,7 @@ import { environment } from '@environments/environment';
export class DiceService { export class DiceService {
private apiUrl = `${environment.apiUrl}/dice`; private apiUrl = `${environment.apiUrl}/dice`;
private http = inject(HttpClient); constructor(private http: HttpClient) {}
rollDice(diceDto: DiceDto): Observable<DiceResult> { rollDice(diceDto: DiceDto): Observable<DiceResult> {
return this.http.post<DiceResult>(this.apiUrl, diceDto); return this.http.post<DiceResult>(this.apiUrl, diceDto);

View file

@ -4,7 +4,7 @@
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-4"> <div class="lg:col-span-3">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h3 class="section-heading text-2xl">Alle Spiele</h3> <h3 class="section-heading text-2xl">Alle Spiele</h3>
<div class="flex space-x-2"> <div class="flex space-x-2">
@ -18,10 +18,8 @@
</div> </div>
<div class="slider-container"> <div class="slider-container">
<div class="min-w-full space-y-4"> <div class="slider-grid">
<!-- Top row with 3 games --> <div class="card group" *ngFor="let game of featuredGames">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="card group" *ngFor="let game of featuredGames.slice(0, 3)">
<div class="relative overflow-hidden rounded-lg"> <div class="relative overflow-hidden rounded-lg">
<img <img
[src]="game.image" [src]="game.image"
@ -43,32 +41,52 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<!-- Bottom row with 2 games centered --> <div class="lg:col-span-1 space-y-6">
<div <div class="card p-4">
class="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto xl:max-w-3xl xl:gap-6" <h3 class="section-heading text-xl mb-4">Konto</h3>
<div class="space-y-4">
<button class="button-primary w-full py-2" (click)="openDepositModal()">Einzahlen</button>
<app-deposit
[isOpen]="isDepositModalOpen"
(closeModalEmitter)="closeDepositModal()"
></app-deposit>
<button
class="bg-deep-blue-light hover:bg-deep-blue-contrast w-full py-2 rounded"
(click)="openTransactionModal()"
> >
<div class="card group" *ngFor="let game of featuredGames.slice(3, 5)"> Transaktionen
<div class="relative overflow-hidden rounded-lg">
<img
[src]="game.image"
[alt]="game.name"
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-deep-blue/95 via-deep-blue/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 ease-in-out"
>
<div
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
>
<h4 class="game-heading">{{ game.name }}</h4>
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
Jetzt Spielen
</button> </button>
<app-transaction-history
[isOpen]="isTransactionModalOpen"
(closeEventEmitter)="closeTransactionModal()"
/>
</div> </div>
</div> </div>
<app-confirmation
[successful]="isDepositSuccessful"
(closeConfirmation)="closeDepositConfirmationModal()"
></app-confirmation>
<div class="card p-4">
<h3 class="section-heading text-xl mb-4">Letzte Transaktionen</h3>
<div class="space-y-3">
<div
class="flex justify-between items-center"
*ngFor="let transaction of (recentTransactionData | async)?.transactions"
>
<div>
<p class="text-sm font-medium">{{ transaction.status }}</p>
<p class="text-xs text-text-secondary">
{{ transaction.createdAt | date: 'd.m.y H:m' }}
</p>
</div> </div>
</div> <span [class]="transaction.amount > 0 ? 'text-emerald' : 'text-accent-red'">
{{ transaction.amount | currency: 'EUR' }}
</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,21 +1,39 @@
import { ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { NgFor } from '@angular/common'; import { AsyncPipe, CurrencyPipe, DatePipe, NgFor } from '@angular/common';
import { DepositComponent } from '../deposit/deposit.component';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ConfirmationComponent } from '@shared/components/confirmation/confirmation.component';
import { Game } from 'app/model/Game'; import { Game } from 'app/model/Game';
import { Observable } from 'rxjs';
import { TransactionService } from '@service/transaction.service';
import format from 'ajv/dist/vocabularies/format'; import format from 'ajv/dist/vocabularies/format';
import { TransactionHistoryComponent } from '../transaction-history/transaction-history.component';
import { TransactionData } from '../../model/TransactionData';
@Component({ @Component({
selector: 'app-homepage', selector: 'app-homepage',
standalone: true, standalone: true,
imports: [NgFor], imports: [
CurrencyPipe,
NgFor,
DepositComponent,
ConfirmationComponent,
AsyncPipe,
DatePipe,
TransactionHistoryComponent,
],
templateUrl: './home.component.html', templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export default class HomeComponent implements OnInit { export default class HomeComponent implements OnInit {
isDepositModalOpen = false;
isDepositSuccessful = false; isDepositSuccessful = false;
isTransactionModalOpen = false;
public route = inject(ActivatedRoute); constructor(
public router = inject(Router); public route: ActivatedRoute,
public router: Router
) {}
ngOnInit() { ngOnInit() {
this.isDepositSuccessful = this.route.snapshot.queryParams['success'] == 'true'; this.isDepositSuccessful = this.route.snapshot.queryParams['success'] == 'true';
@ -60,10 +78,35 @@ export default class HomeComponent implements OnInit {
}, },
]; ];
allGames: Game[] = [...this.featuredGames];
recentTransactionData: Observable<TransactionData> =
inject(TransactionService).getUsersTransactions(5);
openDepositModal() {
this.isDepositModalOpen = true;
}
closeDepositModal() {
this.isDepositModalOpen = false;
}
openDepositConfirmationModal() { openDepositConfirmationModal() {
this.isDepositSuccessful = true; this.isDepositSuccessful = true;
} }
openTransactionModal() {
this.isTransactionModalOpen = true;
}
closeDepositConfirmationModal() {
this.isDepositSuccessful = false;
}
closeTransactionModal() {
this.isTransactionModalOpen = false;
}
navigateToGame(route: string) { navigateToGame(route: string) {
this.router.navigate([route]); this.router.navigate([route]);
} }

View file

@ -21,7 +21,13 @@
(click)="showRegisterForm()" (click)="showRegisterForm()"
class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg" class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg"
> >
Jetzt registrieren Konto erstellen
</button>
<button
(click)="showLoginForm()"
class="w-full sm:w-auto bg-slate-700 text-white hover:bg-slate-600 px-6 sm:px-8 py-3 shadow-lg rounded"
>
Anmelden
</button> </button>
} }
</div> </div>
@ -40,108 +46,58 @@
<div class="game-card-content"> <div class="game-card-content">
<h3 class="game-heading-sm">Slots</h3> <h3 class="game-heading-sm">Slots</h3>
<p class="game-text">Klassische Spielautomaten</p> <p class="game-text">Klassische Spielautomaten</p>
@if (isLoggedIn()) {
<a <a
routerLink="game/slots" routerLink="game/slots"
class="button-primary w-full py-2 inline-block text-center" class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
> >
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
</div> </div>
</div> </div>
<div class="hidden lg:block card"> <div class="hidden lg:block card">
<div class="game-card-content"> <div class="game-card-content">
<h3 class="game-heading-sm">Blackjack</h3> <h3 class="game-heading-sm">Blackjack</h3>
<p class="game-text">Klassisches Kartenspiel</p> <p class="game-text">Klassisches Kartenspiel</p>
@if (isLoggedIn()) {
<a <a
routerLink="game/blackjack" routerLink="game/blackjack"
class="button-primary w-full py-2 inline-block text-center" class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
> >
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
</div>
</div>
<div class="hidden lg:block card">
<div class="game-card-content">
<h3 class="game-heading-sm">Coinflip</h3>
<p class="game-text">Münzwurf</p>
@if (isLoggedIn()) {
<a
routerLink="game/coinflip"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
</div> </div>
</div> </div>
</div> </div>
<div class="slider-grid"> <div class="slider-grid">
<div class="card">
<div class="game-card-content">
<h3 class="game-heading-sm">Poker</h3>
<p class="game-text">Texas Hold'em & mehr</p>
<a
routerLink="/game/poker"
class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
>
</div>
</div>
<div class="card"> <div class="card">
<div class="game-card-content"> <div class="game-card-content">
<h3 class="game-heading-sm">Dice</h3> <h3 class="game-heading-sm">Dice</h3>
<p class="game-text">Würfelspiel</p> <p class="game-text">Würfelspiel</p>
@if (isLoggedIn()) {
<a <a
routerLink="game/dice" routerLink="/game/dice"
class="button-primary w-full py-2 inline-block text-center" class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
> >
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
</div> </div>
</div> </div>
<div class="hidden lg:block card"> <div class="hidden lg:block card">
<div class="game-card-content"> <div class="game-card-content">
<h3 class="game-heading-sm">Lootboxen</h3> <h3 class="game-heading-sm">Lootboxen</h3>
<p class="game-text">Überraschungskisten</p> <p class="game-text">Überraschungskisten</p>
@if (isLoggedIn()) {
<a <a
routerLink="game/lootboxes" routerLink="game/lootboxes"
class="button-primary w-full py-2 inline-block text-center" class="button-primary w-full py-2 inline-block text-center"
>Jetzt Spielen</a
> >
Jetzt Spielen
</a>
} @else {
<button
(click)="showLoginForm()"
class="button-primary w-full py-2 inline-block text-center"
>
Jetzt Spielen
</button>
}
</div> </div>
</div> </div>
</div> </div>
@ -209,7 +165,7 @@
<div class="stat-container"> <div class="stat-container">
<div class="stat-number">24/7</div> <div class="stat-number">24/7</div>
<div class="stat-text">Support</div> <div class="stat-text">Support <span class="text-emerald text-xs">*</span></div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -23,14 +23,15 @@ import RecoverPasswordComponent from '../auth/recover-password/recover-password.
}) })
export class LandingComponent implements OnInit, OnDestroy { export class LandingComponent implements OnInit, OnDestroy {
currentSlide = 0; currentSlide = 0;
private autoplayInterval: ReturnType<typeof setInterval> | undefined;
authService: AuthService = inject(AuthService); authService: AuthService = inject(AuthService);
route: ActivatedRoute = inject(ActivatedRoute); route: ActivatedRoute = inject(ActivatedRoute);
showLogin = signal(false); showLogin = signal(false);
showRegister = signal(false); showRegister = signal(false);
showRecoverPassword = signal(false); showRecoverPassword = signal(false);
isLoggedIn = signal(this.authService.isLoggedIn());
ngOnInit() { ngOnInit() {
this.startAutoplay();
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto';
if (this.route.snapshot.queryParamMap.get('login') === 'true') { if (this.route.snapshot.queryParamMap.get('login') === 'true') {
this.showLoginForm(); this.showLoginForm();
@ -38,6 +39,7 @@ export class LandingComponent implements OnInit, OnDestroy {
} }
ngOnDestroy() { ngOnDestroy() {
this.stopAutoplay();
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto';
} }
@ -71,13 +73,33 @@ export class LandingComponent implements OnInit, OnDestroy {
prevSlide() { prevSlide() {
this.currentSlide = this.currentSlide === 0 ? 1 : 0; this.currentSlide = this.currentSlide === 0 ? 1 : 0;
this.resetAutoplay();
} }
nextSlide() { nextSlide() {
this.currentSlide = this.currentSlide === 1 ? 0 : 1; this.currentSlide = this.currentSlide === 1 ? 0 : 1;
this.resetAutoplay();
} }
goToSlide(index: number) { goToSlide(index: number) {
this.currentSlide = index; this.currentSlide = index;
this.resetAutoplay();
}
private startAutoplay() {
this.autoplayInterval = setInterval(() => {
this.nextSlide();
}, 5000);
}
private stopAutoplay() {
if (this.autoplayInterval) {
clearInterval(this.autoplayInterval);
}
}
private resetAutoplay() {
this.stopAutoplay();
this.startAutoplay();
} }
} }

View file

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, inject } from '@angular/core'; import { ChangeDetectorRef, Component } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { LootboxService } from '../services/lootbox.service'; import { LootboxService } from '../services/lootbox.service';
@ -26,14 +26,14 @@ export default class LootboxOpeningComponent {
currentUser: User | null = null; currentUser: User | null = null;
private winSound: HTMLAudioElement; private winSound: HTMLAudioElement;
private route = inject(ActivatedRoute); constructor(
private router = inject(Router); private route: ActivatedRoute,
private lootboxService = inject(LootboxService); private router: Router,
private userService = inject(UserService); private lootboxService: LootboxService,
private authService = inject(AuthService); private userService: UserService,
private cdr = inject(ChangeDetectorRef); private authService: AuthService,
private cdr: ChangeDetectorRef
constructor() { ) {
this.winSound = new Audio('/sounds/win.mp3'); this.winSound = new Audio('/sounds/win.mp3');
this.loadLootbox(); this.loadLootbox();
this.authService.userSubject.subscribe((user) => { this.authService.userSubject.subscribe((user) => {

View file

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, OnInit, inject } from '@angular/core'; import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { LootboxService } from '../services/lootbox.service'; import { LootboxService } from '../services/lootbox.service';
import { LootBox } from 'app/model/LootBox'; import { LootBox } from 'app/model/LootBox';
@ -86,11 +86,13 @@ export default class LootboxSelectionComponent implements OnInit {
}, },
]; ];
private lootboxService = inject(LootboxService); constructor(
private router = inject(Router); private lootboxService: LootboxService,
private cdr = inject(ChangeDetectorRef); private router: Router,
private authService = inject(AuthService); private cdr: ChangeDetectorRef,
private userService = inject(UserService); private authService: AuthService,
private userService: UserService
) {}
ngOnInit(): void { ngOnInit(): void {
this.loadLootboxes(); this.loadLootboxes();

View file

@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs'; import { BehaviorSubject, Observable, tap } from 'rxjs';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
@ -19,13 +19,13 @@ export class AuthService {
private userUrl = `${environment.apiUrl}/users`; private userUrl = `${environment.apiUrl}/users`;
private oauthUrl = `${environment.apiUrl}/oauth2`; private oauthUrl = `${environment.apiUrl}/oauth2`;
private http = inject(HttpClient);
private router = inject(Router);
private route = inject(ActivatedRoute);
userSubject: BehaviorSubject<User | null>; userSubject: BehaviorSubject<User | null>;
constructor() { constructor(
private http: HttpClient,
private router: Router,
private route: ActivatedRoute
) {
this.userSubject = new BehaviorSubject<User | null>(this.getUserFromStorage()); this.userSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
// Check for token in URL (OAuth callback) on initialization // Check for token in URL (OAuth callback) on initialization

View file

@ -7,7 +7,6 @@ import {
OnDestroy, OnDestroy,
Output, Output,
ViewChild, ViewChild,
inject,
} from '@angular/core'; } from '@angular/core';
import { ModalAnimationService } from '@shared/services/modal-animation.service'; import { ModalAnimationService } from '@shared/services/modal-animation.service';
import gsap from 'gsap'; import gsap from 'gsap';
@ -24,7 +23,7 @@ export class ConfirmationComponent implements AfterViewInit, OnDestroy {
@ViewChild('modalBg') modalBg!: ElementRef; @ViewChild('modalBg') modalBg!: ElementRef;
@ViewChild('modalCard') modalCard!: ElementRef; @ViewChild('modalCard') modalCard!: ElementRef;
private modalAnimationService = inject(ModalAnimationService); constructor(private modalAnimationService: ModalAnimationService) {}
ngAfterViewInit() { ngAfterViewInit() {
if (this.successful) { if (this.successful) {

View file

@ -5,19 +5,24 @@
<h3 class="footer-heading">Casino Spiele</h3> <h3 class="footer-heading">Casino Spiele</h3>
<ul class="space-y-3"> <ul class="space-y-3">
<li> <li>
<a routerLink="/game/slots" class="footer-link">Slots</a> <a routerLink="/games" class="footer-link">Slots</a>
</li> </li>
<li> <li>
<a routerLink="/game/blackjack" class="footer-link">Blackjack</a> <a routerLink="/games" class="footer-link">Blackjack</a>
</li> </li>
<li> <li>
<a routerLink="/game/dice" class="footer-link">Dice</a> <a routerLink="/games" class="footer-link">Poker</a>
</li> </li>
<li> <li>
<a routerLink="/game/coinflip" class="footer-link">Coinflip</a> <a routerLink="/games" class="footer-link">Liars Dice</a>
</li> </li>
</ul>
</div>
<div class="footer-section">
<h3 class="footer-heading">Andere Spiele</h3>
<ul class="space-y-3">
<li> <li>
<a routerLink="/game/lootboxes" class="footer-link">Lootboxen</a> <a routerLink="/games" class="footer-link">Lootboxen</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -49,6 +54,10 @@
<fa-icon [icon]="faGooglePay" class="footer-payment-icon"></fa-icon> <fa-icon [icon]="faGooglePay" class="footer-payment-icon"></fa-icon>
<span class="footer-payment-text">Google Pay</span> <span class="footer-payment-text">Google Pay</span>
</div> </div>
<div class="footer-payment-method">
<fa-icon [icon]="faApplePay" class="footer-payment-icon"></fa-icon>
<span class="footer-payment-text">Apple Pay</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -56,6 +65,10 @@
<div class="mt-12 pt-8 border-t border-deep-blue-light"> <div class="mt-12 pt-8 border-t border-deep-blue-light">
<div class="flex flex-col md:flex-row justify-between items-center"> <div class="flex flex-col md:flex-row justify-between items-center">
<div class="footer-copyright"> <div class="footer-copyright">
<span class="footer-disclaimer">
<span class="text-emerald">*</span> nicht vorhanden.
</span>
<br />
© {{ currentYear }} Trustworthy Casino. Keine Rechte vorbehalten. © {{ currentYear }} Trustworthy Casino. Keine Rechte vorbehalten.
</div> </div>
</div> </div>

View file

@ -1,14 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { faCreditCard, faMoneyBillTransfer, faWallet } from '@fortawesome/free-solid-svg-icons'; import { faCreditCard, faMoneyBillTransfer, faWallet } from '@fortawesome/free-solid-svg-icons';
import { faGooglePay, faPaypal } from '@fortawesome/free-brands-svg-icons'; import { faApplePay, faGooglePay, faPaypal } from '@fortawesome/free-brands-svg-icons';
import { RouterLink } from '@angular/router';
@Component({ @Component({
selector: 'app-footer', selector: 'app-footer',
standalone: true, standalone: true,
templateUrl: './footer.component.html', templateUrl: './footer.component.html',
imports: [FontAwesomeModule, RouterLink], imports: [FontAwesomeModule],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FooterComponent { export class FooterComponent {
@ -19,4 +18,5 @@ export class FooterComponent {
faMoneyBillTransfer = faMoneyBillTransfer; faMoneyBillTransfer = faMoneyBillTransfer;
faWallet = faWallet; faWallet = faWallet;
faGooglePay = faGooglePay; faGooglePay = faGooglePay;
faApplePay = faApplePay;
} }

View file

@ -1,120 +1,40 @@
<nav class="bg-deep-blue-light border-b border-emerald-500/30 shadow-lg"> <nav class="bg-deep-blue border-b border-deep-blue-contrast">
<div class="max-w-full mx-auto px-6"> <div class="max-w-full mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-14">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-6">
<a routerLink="/" class="flex items-center space-x-3 group"> <a routerLink="/" class="nav-brand">
<div class="flex flex-col"> <span>Trustworthy Casino</span>
<span class="text-xl font-bold text-white"> Trustworthy Casino </span>
<span class="text-xs text-emerald-400 font-medium">Trust. Play. Win.</span>
</div>
</a> </a>
<div class="hidden md:flex items-center space-x-1"> <div class="hidden md:flex items-center space-x-1">
@if (isLoggedIn()) { <a routerLink="/home" class="nav-link">Spiele</a>
<a
routerLink="/home"
class="flex items-center px-4 py-2 text-white/90 hover:text-white font-medium rounded-lg hover:bg-white/10 transition-colors duration-200"
>
<img class="mr-2 w-4 h-4" src="assets/games.svg" alt="gamess" />
Spiele
</a>
} @else {
<button
(click)="showLogin.emit()"
class="flex items-center px-4 py-2 text-white/90 hover:text-white font-medium rounded-lg hover:bg-white/10 transition-colors duration-200"
>
<img class="mr-2 w-4 h-4" src="assets/games.svg" alt="gamess" />
Spiele
</button>
}
</div> </div>
</div> </div>
<div class="hidden md:flex items-center space-x-1"> <div class="hidden md:flex items-center space-x-4">
@if (!isLoggedIn()) { @if (!isLoggedIn()) {
<button <button (click)="showLogin.emit()" class="button-primary px-4 py-1.5">Anmelden</button>
(click)="showLogin.emit()"
class="flex items-center px-4 py-2 text-white font-medium border border-emerald-500 rounded-lg hover:bg-emerald-500/10 transition-colors duration-200"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
Anmelden
</button>
<button <button
(click)="showRegister.emit()" (click)="showRegister.emit()"
class="flex items-center px-4 py-2 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200" class="bg-emerald-700 text-white hover:bg-emerald-600 px-4 py-1.5 rounded"
> >
Jetzt registrieren Registrieren
</button> </button>
} }
@if (isLoggedIn()) { @if (isLoggedIn()) {
<div <div
class="flex items-center px-4 py-2 mr-2 bg-slate-700 border border-emerald-500/30 rounded-lg font-medium" class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300"
> routerLink="/home"
<span class="text-emerald-400 text-sm mr-2">Guthaben:</span>
<span
[class]="balance() < 0 ? 'text-red-400 font-bold' : 'text-white font-bold'"
class="text-sm"
> >
<span [class]="balance() < 0 ? 'text-accent-red' : ''">
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number> <app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
</span> </span>
</div> </div>
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
<button
class="flex items-center px-4 py-2 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200"
(click)="openDepositModal()"
>
<img class="mr-2 w-3 h-3" src="assets/deposit.svg" alt="deposits" />
Einzahlen
</button>
<app-deposit
[isOpen]="isDepositModalOpen"
(closeModalEmitter)="closeDepositModal()"
></app-deposit>
<button
class="flex items-center px-4 py-2 bg-slate-700 text-white font-medium rounded-lg hover:bg-slate-600 border border-slate-600 transition-colors duration-200"
(click)="openTransactionModal()"
>
<img class="mr-2 w-4 h-4" src="assets/transaction.svg" alt="transactions" />
Transaktionen
</button>
<app-transaction-history
[isOpen]="isTransactionModalOpen"
(closeEventEmitter)="closeTransactionModal()"
/>
<button
(click)="logout()"
class="flex items-center px-4 py-2 text-red-400 font-medium border border-red-500/50 rounded-lg hover:bg-red-500/10 transition-colors duration-200"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Abmelden
</button>
} }
</div> </div>
<div class="md:hidden"> <div class="md:hidden">
<button <button (click)="toggleMenu()" class="nav-toggle">
(click)="toggleMenu()"
class="p-2 text-white hover:text-emerald-400 rounded-lg transition-colors duration-200"
>
<svg <svg
class="h-6 w-6" class="h-6 w-6"
[class.hidden]="isMenuOpen" [class.hidden]="isMenuOpen"
@ -148,95 +68,25 @@
</div> </div>
<div [class]="isMenuOpen ? 'block' : 'hidden'" class="md:hidden"> <div [class]="isMenuOpen ? 'block' : 'hidden'" class="md:hidden">
<div class="px-2 pt-2 pb-4 space-y-3 bg-slate-700 rounded-lg mt-2"> <div class="nav-mobile-menu">
<a <a routerLink="/games" class="nav-mobile-link">Spiele</a>
routerLink="/home" <div class="pt-2 space-y-2">
class="flex items-center px-4 py-3 text-white/90 hover:text-white hover:bg-white/10 rounded-lg transition-colors duration-200"
>
<img class="mr-2 w-4 h-4" src="assets/games.svg" alt="gamess" />
Spiele
</a>
<div class="border-t border-slate-600 pt-3 space-y-3">
@if (!isLoggedIn()) { @if (!isLoggedIn()) {
<button <button
(click)="showLogin.emit()" (click)="showLogin.emit()"
class="w-full flex items-center justify-center px-4 py-3 text-white font-medium border border-emerald-500 rounded-lg hover:bg-emerald-500/10 transition-colors duration-200" class="button-primary w-full py-1.5 block text-center"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
/>
</svg>
Anmelden Anmelden
</button> </button>
<button <button
(click)="showRegister.emit()" (click)="showRegister.emit()"
class="w-full flex items-center justify-center px-4 py-3 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200" class="bg-emerald-700 text-white hover:bg-emerald-600 w-full py-1.5 rounded block text-center"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/>
</svg>
Registrieren Registrieren
</button> </button>
} }
@if (isLoggedIn()) { @if (isLoggedIn()) {
<div <button (click)="logout()" class="button-primary w-full py-1.5">Abmelden</button>
class="flex items-center justify-center px-4 py-3 bg-slate-700 border border-emerald-500/30 rounded-lg"
>
<span class="text-emerald-400 text-sm font-medium mr-2">Guthaben:</span>
<span
[class]="balance() < 0 ? 'text-red-400 font-bold' : 'text-white font-bold'"
class="text-sm"
>
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
</span>
</div>
<button
(click)="openDepositModal()"
class="w-full flex items-center justify-center px-4 py-3 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
Einzahlen
</button>
<button
(click)="openTransactionModal()"
class="w-full flex items-center justify-center px-4 py-3 bg-slate-700 text-white font-medium rounded-lg hover:bg-slate-600 border border-slate-600 transition-colors duration-200"
>
Transaktionen
</button>
<button
(click)="logout()"
class="w-full flex items-center justify-center px-4 py-3 text-red-400 font-medium border border-red-500/50 rounded-lg hover:bg-red-500/10 transition-colors duration-200"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Abmelden
</button>
} }
</div> </div>
</div> </div>

View file

@ -12,20 +12,16 @@ import { RouterModule } from '@angular/router';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component'; import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
import { DepositComponent } from '../../../feature/deposit/deposit.component';
import { TransactionHistoryComponent } from '../../../feature/transaction-history/transaction-history.component';
@Component({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
standalone: true, standalone: true,
imports: [RouterModule, AnimatedNumberComponent, DepositComponent, TransactionHistoryComponent], imports: [RouterModule, AnimatedNumberComponent],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NavbarComponent implements OnInit, OnDestroy { export class NavbarComponent implements OnInit, OnDestroy {
isMenuOpen = false; isMenuOpen = false;
isDepositModalOpen = false;
isTransactionModalOpen = false;
private authService: AuthService = inject(AuthService); private authService: AuthService = inject(AuthService);
isLoggedIn = signal(this.authService.isLoggedIn()); isLoggedIn = signal(this.authService.isLoggedIn());
@ -55,20 +51,4 @@ export class NavbarComponent implements OnInit, OnDestroy {
toggleMenu() { toggleMenu() {
this.isMenuOpen = !this.isMenuOpen; this.isMenuOpen = !this.isMenuOpen;
} }
openDepositModal() {
this.isDepositModalOpen = true;
}
closeDepositModal() {
this.isDepositModalOpen = false;
}
openTransactionModal() {
this.isTransactionModalOpen = true;
}
closeTransactionModal() {
this.isTransactionModalOpen = false;
}
} }

View file

@ -1,4 +1,4 @@
import { Injectable, Renderer2, RendererFactory2, inject } from '@angular/core'; import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -7,10 +7,8 @@ export class SoundInitializerService {
private renderer: Renderer2; private renderer: Renderer2;
private observer: MutationObserver; private observer: MutationObserver;
private rendererFactory = inject(RendererFactory2); constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
constructor() {
this.renderer = this.rendererFactory.createRenderer(null, null);
this.observer = new MutationObserver((mutations) => { this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {

View file

@ -1,4 +0,0 @@
<svg style="color: white;" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
<path fill="currentColor" d="m8 16l-2-3h1v-2h2v2h1zm7-15v8H1V1zm1-1H0v10h16z"/>
<path fill="currentColor" d="M8 2a3 3 0 1 1 0 6h5V7h1V3h-1V2zM5 5a3 3 0 0 1 3-3H3v1H2v4h1v1h5a3 3 0 0 1-3-3"/>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

View file

@ -1,6 +0,0 @@
<svg style="color: white;" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512">
<path fill="currentColor"
d="M495.24 267.592L445.066 41.083A32.04 32.04 0 0 0 406.9 16.76L180.393 66.934a32 32 0 0 0-24.322 38.166l21.021 94.9H48a32.036 32.036 0 0 0-32 32v232a32.036 32.036 0 0 0 32 32h232a32.036 32.036 0 0 0 32-32V340.957l158.917-35.2a32.04 32.04 0 0 0 24.323-38.165M280 464H48V232h136.181l22.063 99.606a32.03 32.03 0 0 0 31.18 25.092a32.3 32.3 0 0 0 6.984-.769l35.6-7.886L280.02 464Zm184-189.487l-226.513 50.173l-50.173-226.51L413.824 48l50.193 226.505Z"/>
<path fill="currentColor"
d="M80 264h40v40H80zm0 128h40v40H80zm128 0h40v40h-40zm-64-64h40v40h-40zm81.456-205.433l39.054-8.644l8.644 39.055l-39.054 8.644zm152.672 97.223l39.054-8.65l8.65 39.054l-39.054 8.65zm-76.324-48.649l39.053-8.65l8.65 39.053l-39.052 8.65z"/>
</svg>

Before

Width:  |  Height:  |  Size: 882 B

View file

@ -1,12 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
style="color: white;"
>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
<path d="M4.58 8.607L2 8.454C3.849 3.704 9.158 1 14.333 2.344c5.513 1.433 8.788 6.918 7.314 12.25c-1.219 4.411-5.304 7.337-9.8 7.406"/>
<path d="M12 22C6.5 22 2 17 2 11m11.604-1.278c-.352-.37-1.213-1.237-2.575-.62c-1.361.615-1.577 2.596.482 2.807c.93.095 1.537-.11 2.093.47c.556.582.659 2.198-.761 2.634s-2.341-.284-2.588-.509m1.653-6.484v.79m0 6.337v.873"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 639 B

View file

@ -71,10 +71,6 @@ a {
@apply font-bold text-text-primary text-sm mb-2; @apply font-bold text-text-primary text-sm mb-2;
} }
.game-heading {
@apply font-bold text-text-primary text-lg mb-2;
}
.game-heading-xl { .game-heading-xl {
@apply font-bold text-text-primary text-xl mb-2; @apply font-bold text-text-primary text-xl mb-2;
} }

View file

@ -6,7 +6,8 @@ info:
start: start:
command -v concurrently &> /dev/null || bun add -g concurrently command -v concurrently &> /dev/null || bun add -g concurrently
command -v watchexec &> /dev/null || brew install watchexec command -v watchexec &> /dev/null || brew install watchexec
conc -n "frontend,backend,docker" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun" "docker compose up" docker compose up -d
conc -n "frontend,backend" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun"
# Builds both the backend and frontend docker images (obv) # Builds both the backend and frontend docker images (obv)
build: build:

View file

@ -1,132 +0,0 @@
## Core latex/pdflatex auxiliary files:
*.aux
*.lof
*.log
*.lot
*.fls
*.out
*.toc
*.pdf
## Intermediate documents:
*.dvi
*-converted-to.*
# these rules might exclude image files for figures etc.
# *.ps
# *.eps
# *.pdf
## Bibliography auxiliary files (bibtex/biblatex/biber):
*.bbl
*.bcf
*.blg
*-blx.aux
*-blx.bib
*.brf
*.run.xml
## Build tool auxiliary files:
*.fdb_latexmk
*.synctex
*.synctex.gz
*.synctex.gz(busy)
*.pdfsync
## Auxiliary and intermediate files from other packages:
# algorithms
*.alg
*.loa
# achemso
acs-*.bib
# amsthm
*.thm
# beamer
*.nav
*.snm
*.vrb
#(e)ledmac/(e)ledpar
*.end
*.[1-9]
*.[1-9][0-9]
*.[1-9][0-9][0-9]
*.[1-9]R
*.[1-9][0-9]R
*.[1-9][0-9][0-9]R
*.eledsec[1-9]
*.eledsec[1-9]R
*.eledsec[1-9][0-9]
*.eledsec[1-9][0-9]R
*.eledsec[1-9][0-9][0-9]
*.eledsec[1-9][0-9][0-9]R
# glossaries
*.acn
*.acr
*.glg
*.glo
*.gls
# gnuplottex
*-gnuplottex-*
# hyperref
*.brf
# knitr
*-concordance.tex
*.tikz
*-tikzDictionary
# listings
*.lol
# makeidx
*.idx
*.ilg
*.ind
*.ist
# minitoc
*.maf
*.mtc
*.mtc0
# minted
_minted*
*.pyg
# morewrites
*.mw
# nomencl
*.nlo
# sagetex
*.sagetex.sage
*.sagetex.py
*.sagetex.scmd
# sympy
*.sout
*.sympy
sympy-plots-for-*.tex/
# todonotes
*.tdo
# xindy
*.xdy
# WinEdt
*.bak
*.sav
/tmp/
# idea
.idea/
Projektdokumentation.syntex(busy)

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>VorlageFachinformatiker</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>net.sourceforge.texlipse.builder.TexlipseBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>net.sourceforge.texlipse.builder.TexlipseNature</nature>
</natures>
</projectDescription>

View file

@ -1,13 +0,0 @@
#TeXlipse project settings
#Fri Jan 27 09:15:16 CET 2012
builderNum=2
outputDir=
makeIndSty=
bibrefDir=
outputFormat=pdf
tempDir=tmp
mainTexFile=Projektdokumentation.tex
outputFile=Projektdokumentation.pdf
langSpell=de
markDer=true
srcDir=

View file

@ -1,23 +0,0 @@
% !TEX root = Projektdokumentation.tex
% Es werden nur die Abkürzungen aufgelistet, die mit \ac definiert und auch benutzt wurden.
%
% \acro{VERSIS}{Versicherungsinformationssystem\acroextra{ (Bestandsführungssystem)}}
% Ergibt in der Liste: VERSIS Versicherungsinformationssystem (Bestandsführungssystem)
% Im Text aber: \ac{VERSIS} -> Versicherungsinformationssystem (VERSIS)
% Hinweis: allgemein bekannte Abkürzungen wie z.B. bzw. u.a. müssen nicht ins Abkürzungsverzeichnis aufgenommen werden
% Hinweis: allgemein bekannte IT-Begriffe wie Datenbank oder Programmiersprache müssen nicht erläutert werden,
% aber ggfs. Fachbegriffe aus der Domäne des Prüflings (z.B. Versicherung)
% Die Option (in den eckigen Klammern) enthält das längste Label oder
% einen Platzhalter der die Breite der linken Spalte bestimmt.
\begin{acronym}[WWWWW]
\acro{CI}{Continuous Integration}
\acro{CI/CD}{Continuous Integration/Continuous Deployment}
\acro{E2E}{End-to-End}
\acro{API}{Application Programming Interface}
\acro{JSON}{JavaScript Object Notation}
\acro{HTTP}{Hypertext Transfer Protocol}
\acro{JWT}{JSON Web Token}
\end{acronym}

View file

@ -1,85 +0,0 @@
% !TEX root = ../Projektdokumentation.tex
% Abkürzungen, ggfs. mit korrektem Leerraum
\newcommand{\bs}{$\backslash$\xspace}
\newcommand{\bspw}{bspw.\xspace}
\newcommand{\bzw}{bzw.\xspace}
\newcommand{\ca}{ca.\xspace}
\newcommand{\dahe}{\mbox{d.\,h.}\xspace}
\newcommand{\etc}{etc.\xspace}
\newcommand{\eur}[1]{\mbox{#1\,\texteuro}\xspace}
\newcommand{\evtl}{evtl.\xspace}
\newcommand{\ggfs}{ggfs.\xspace}
\newcommand{\Ggfs}{Ggfs.\xspace}
\newcommand{\gqq}[1]{\glqq{}#1\grqq{}}
\newcommand{\inkl}{inkl.\xspace}
\newcommand{\insb}{insb.\xspace}
\newcommand{\ua}{\mbox{u.\,a.}\xspace}
\newcommand{\usw}{usw.\xspace}
\newcommand{\Vgl}{Vgl.\xspace}
\newcommand{\zB}{\mbox{z.\,B.}\xspace}
% Befehle für häufig anfallende Aufgaben
\newcommand{\Abbildung}[1]{\autoref{fig:#1}}
\newcommand{\Anhang}[1]{\appendixname{}~\ref{#1}: \nameref{#1} \vpageref{#1}}
\newcommand{\includegraphicsKeepAspectRatio}[2]{\includegraphics[width=#2\textwidth,height=#2\textheight,keepaspectratio]{#1}}
\newcommand{\Zitat}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\citep{#2}}{\citep[#1]{#2}}}
\newcommand{\Autor}[1]{\textsc{#1}} % zum Ausgeben von Autoren
\newcommand{\itemd}[2]{\item{\textbf{#1}}\\{#2}} % erzeugt ein Listenelement mit fetter Überschrift
% fügt Tabellen aus einer TEX-Datei ein
\newcommand{\tabelle}[3] % Parameter: caption, label, file
{\begin{table}[htbp]
\centering
\singlespacing
\input{Tabellen/#3}
\caption{#1}
\label{#2}
\end{table}}
\newcommand{\tabelleAnhang}[1] % Parameter: file
{\begin{center}
\singlespacing
\input{Tabellen/#1}
\end{center}}
% einfaches Wechseln der Schrift, z.B.: \changefont{cmss}{sbc}{n}
\newcommand{\changefont}[3]{\fontfamily{#1} \fontseries{#2} \fontshape{#3} \selectfont}
% Verwendung analog zu \includegraphics
\newlength{\myx} % Variable zum Speichern der Bildbreite
\newlength{\myy} % Variable zum Speichern der Bildhöhe
\newcommand\includegraphicstotab[2][\relax]{%
% Abspeichern der Bildabmessungen
\settowidth{\myx}{\includegraphics[{#1}]{#2}}%
\settoheight{\myy}{\includegraphics[{#1}]{#2}}%
% das eigentliche Einfügen
\parbox[c][1.1\myy][c]{\myx}{%
\includegraphics[{#1}]{#2}}%
}
\definecolor{AOBlau}{rgb}{0, 0.28, 0.56}
% verschiedene Befehle um Wörter semantisch auszuzeichnen ----------------------
\newcommand{\Index}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\index{#2}#2}{\index{#1}#2}}
\newcommand{\Fachbegriff}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\textit{\Index{#2}}}{\textit{\Index[#1]{#2}}}}
\newcommand{\NeuerBegriff}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\textbf{\Index{#2}}}{\textbf{\Index[#1]{#2}}}}
\newcommand{\Ausgabe}[1]{\texttt{#1}}
\newcommand{\Eingabe}[1]{\texttt{#1}}
\newcommand{\Code}[1]{\texttt{#1}}
\newcommand{\Datei}[1]{\texttt{#1}}
\newcommand{\Assembly}[1]{\textsf{#1}}
\newcommand{\Klasse}[1]{\textsf{#1}}
\newcommand{\Methode}[1]{\textsf{#1}}
\newcommand{\Attribut}[1]{\textsf{#1}}
\newcommand{\Datentyp}[1]{\textsf{#1}}
\newcommand{\XMLElement}[1]{\textsf{#1}}
\newcommand{\Webservice}[1]{\textsf{#1}}
\newcommand{\Refactoring}[1]{\Fachbegriff{#1}}
\newcommand{\CodeSmell}[1]{\Fachbegriff{#1}}
\newcommand{\Metrik}[1]{\Fachbegriff{#1}}
\newcommand{\DesignPattern}[1]{\Fachbegriff{#1}}

View file

@ -1,196 +0,0 @@
% !TEX root = ../Projektdokumentation.tex
% Anpassung an Landessprache ---------------------------------------------------
\usepackage{babel}
% Umlaute ----------------------------------------------------------------------
% Umlaute/Sonderzeichen wie äüöß direkt im Quelltext verwenden (CodePage).
% Erlaubt automatische Trennung von Worten mit Umlauten.
% ------------------------------------------------------------------------------
\usepackage[T1]{fontenc}
\usepackage{textcomp} % Euro-Zeichen etc.
% Schrift ----------------------------------------------------------------------
\usepackage{lmodern} % bessere Fonts
\usepackage{relsize} % Schriftgröße relativ festlegen
% Tabellen ---------------------------------------------------------------------
\PassOptionsToPackage{table}{xcolor}
\usepackage{tabularx}
% für lange Tabellen
\usepackage{longtable}
\usepackage{array}
\usepackage{ragged2e}
\usepackage{lscape}
\newcolumntype{w}[1]{>{\raggedleft\hspace{0pt}}p{#1}} % Spaltendefinition rechtsbündig mit definierter Breite
% Grafiken ---------------------------------------------------------------------
\usepackage[dvips,final]{graphicx} % Einbinden von JPG-Grafiken ermöglichen
\usepackage{graphics} % keepaspectratio
\usepackage{floatflt} % zum Umfließen von Bildern
\graphicspath{{Bilder/}} % hier liegen die Bilder des Dokuments
% Sonstiges --------------------------------------------------------------------
\usepackage[titles]{tocloft} % Inhaltsverzeichnis DIN 5008 gerecht einrücken
% Explicitly set German titles for ToC, LoF, LoT
\renewcommand{\contentsname}{Inhaltsverzeichnis}
\renewcommand{\listfigurename}{Abbildungsverzeichnis}
\renewcommand{\listtablename}{Tabellenverzeichnis}
\usepackage{amsmath,amsfonts} % Befehle aus AMSTeX für mathematische Symbole
\usepackage{enumitem} % anpassbare Enumerates/Itemizes
\usepackage{xspace} % sorgt dafür, dass Leerzeichen hinter parameterlosen Makros nicht als Makroendezeichen interpretiert werden
\usepackage{makeidx} % für Index-Ausgabe mit \printindex
\usepackage[printonlyused]{acronym} % es werden nur benutzte Definitionen aufgelistet
% Einfache Definition der Zeilenabstände und Seitenränder etc.
\usepackage{setspace}
\usepackage{geometry}
% Symbolverzeichnis
\usepackage[intoc]{nomencl}
\let\abbrev\nomenclature
\renewcommand{\nomname}{Abkürzungsverzeichnis}
\setlength{\nomlabelwidth}{.25\hsize}
\renewcommand{\nomlabel}[1]{#1 \dotfill}
\setlength{\nomitemsep}{-\parsep}
\usepackage{varioref} % Elegantere Verweise. „auf der nächsten Seite“
\usepackage{url} % URL verlinken, lange URLs umbrechen etc.
\usepackage{chngcntr} % fortlaufendes Durchnummerieren der Fußnoten
% \usepackage[perpage]{footmisc} % Alternative: Nummerierung der Fußnoten auf jeder Seite neu
\usepackage{ifthen} % bei der Definition eigener Befehle benötigt
\usepackage{todonotes} % definiert u.a. die Befehle \todo und \listoftodos
\usepackage[square]{natbib} % wichtig für korrekte Zitierweise
% PDF-Optionen -----------------------------------------------------------------
\usepackage{pdfpages}
\pdfminorversion=5 % erlaubt das Einfügen von pdf-Dateien bis Version 1.7, ohne eine Fehlermeldung zu werfen (keine Garantie für fehlerfreies Einbetten!)
\usepackage[
bookmarks,
bookmarksnumbered,
bookmarksopen=true,
bookmarksopenlevel=1,
colorlinks=true,
% diese Farbdefinitionen zeichnen Links im PDF farblich aus
linkcolor=AOBlau, % einfache interne Verknüpfungen
anchorcolor=AOBlau,% Ankertext
citecolor=AOBlau, % Verweise auf Literaturverzeichniseinträge im Text
filecolor=AOBlau, % Verknüpfungen, die lokale Dateien öffnen
menucolor=AOBlau, % Acrobat-Menüpunkte
urlcolor=AOBlau,
% diese Farbdefinitionen sollten für den Druck verwendet werden (alles schwarz)
%linkcolor=black, % einfache interne Verknüpfungen
%anchorcolor=black, % Ankertext
%citecolor=black, % Verweise auf Literaturverzeichniseinträge im Text
%filecolor=black, % Verknüpfungen, die lokale Dateien öffnen
%menucolor=black, % Acrobat-Menüpunkte
%urlcolor=black,
%
%backref, % Quellen werden zurück auf ihre Zitate verlinkt
pdftex,
plainpages=false, % zur korrekten Erstellung der Bookmarks
pdfpagelabels=true, % zur korrekten Erstellung der Bookmarks
hypertexnames=false, % zur korrekten Erstellung der Bookmarks
linktocpage % Seitenzahlen anstatt Text im Inhaltsverzeichnis verlinken
]{hyperref}
% Befehle, die Umlaute ausgeben, führen zu Fehlern, wenn sie hyperref als Optionen übergeben werden
\hypersetup{
pdftitle={\titel -- \untertitel},
pdfauthor={\autorName},
pdfcreator={\autorName},
pdfsubject={\titel -- \untertitel},
pdfkeywords={\titel -- \untertitel},
}
% zum Einbinden von Programmcode -----------------------------------------------
\usepackage{listings}
% Explicitly set German title for LoL
\renewcommand{\lstlistlistingname}{Codeverzeichnis} % For list of listings
\usepackage{xcolor}
\definecolor{hellgelb}{rgb}{1,1,0.9}
\definecolor{colKeys}{rgb}{0,0,1}
\definecolor{colIdentifier}{rgb}{0,0,0}
\definecolor{colComments}{rgb}{0,0.5,0}
\definecolor{colString}{rgb}{1,0,0}
\lstset{
float=hbp,
basicstyle=\footnotesize,
identifierstyle=\color{colIdentifier},
keywordstyle=\color{colKeys},
stringstyle=\color{colString},
commentstyle=\color{colComments},
backgroundcolor=\color{hellgelb},
columns=flexible,
tabsize=2,
frame=single,
extendedchars=true,
showspaces=false,
showstringspaces=false,
numbers=left,
numberstyle=\tiny,
breaklines=true,
breakautoindent=true,
captionpos=b,
}
\lstdefinelanguage{cs}{
sensitive=false,
morecomment=[l]{//},
morecomment=[s]{/*}{*/},
morestring=[b]",
morekeywords={
abstract,event,new,struct,as,explicit,null,switch
base,extern,object,this,bool,false,operator,throw,
break,finally,out,true,byte,fixed,override,try,
case,float,params,typeof,catch,for,private,uint,
char,foreach,protected,ulong,checked,goto,public,unchecked,
class,if,readonly,unsafe,const,implicit,ref,ushort,
continue,in,return,using,decimal,int,sbyte,virtual,
default,interface,sealed,volatile,delegate,internal,short,void,
do,is,sizeof,while,double,lock,stackalloc,
else,long,static,enum,namespace,string},
}
\lstdefinelanguage{natural}{
sensitive=false,
morecomment=[l]{/*},
morestring=[b]",
morestring=[b]',
alsodigit={-,*},
morekeywords={
DEFINE,DATA,LOCAL,END-DEFINE,WRITE,CALLNAT,PARAMETER,USING,
IF,NOT,END-IF,ON,*ERROR-NR,ERROR,END-ERROR,ESCAPE,ROUTINE,
PERFORM,SUBROUTINE,END-SUBROUTINE,CONST,END-FOR,END,FOR,RESIZE,
ARRAY,TO,BY,VALUE,RESET,COMPRESS,INTO,EQ},
}
\lstdefinelanguage{php}{
sensitive=false,
morecomment=[l]{/*},
morestring=[b]",
morestring=[b]',
alsodigit={-,*},
morekeywords={
abstract,and,array,as,break,case,catch,cfunction,class,clone,const,
continue,declare,default,do,else,elseif,enddeclare,endfor,endforeach,
endif,endswitch,endwhile,extends,final,for,foreach,function,global,
goto,if,implements,interface,instanceof,namespace,new,old_function,or,
private,protected,public,static,switch,throw,try,use,var,while,xor
die,echo,empty,exit,eval,include,include_once,isset,list,require,
require_once,return,print,unset},
}
\lstdefinelanguage{json}{
basicstyle=\ttfamily\small,
showstringspaces=false,
breaklines=true,
commentstyle=\color{green!50!black},
stringstyle=\color{red},
keywords={true, false, null},
keywordstyle=\color{blue}\bfseries,
morestring=[s]{"}{"},
morecomment=[l]{//},
morecomment=[s]{/*}{*/},
}

View file

@ -1,83 +0,0 @@
% !TEX root = ../Projektdokumentation.tex
% Seitenränder -----------------------------------------------------------------
\setlength{\topskip}{\ht\strutbox} % behebt Warnung von geometry
\geometry{a4paper,left=25mm,right=25mm,top=33mm,bottom=33mm,head=53.14769pt}
\deffootnote{1em}{1em}{\textsuperscript{\thefootnotemark}\fontsize{9pt}{11pt}\selectfont}
\usepackage[
automark, % Kapitelangaben in Kopfzeile automatisch erstellen
headsepline, % Trennlinie unter Kopfzeile
ilines % Trennlinie linksbündig ausrichten
]{scrlayer-scrpage}
% Kopf- und Fußzeilen ----------------------------------------------------------
\pagestyle{scrheadings}
% chapterpagestyle gibt es nicht in scrartcl
%\renewcommand{\chapterpagestyle}{scrheadings}
\clearpairofpagestyles
% Kopfzeile
\renewcommand{\headfont}{\normalfont} % Schriftform der Kopfzeile
\ihead{\large{\textsc{\titel}}\\ \small{\untertitel} \\[2ex] \textit{\headmark}}
\chead{}
\ohead{\includegraphics[scale=0.09]{\betriebLogo}}
%\setheadwidth[0pt]{textwithmarginpar} % Kopfzeile über den Text hinaus verbreitern (falls Logo den Text überdeckt)
% Fußzeile
\cfoot{}
\ofoot{\pagemark}
% Überschriften nach DIN 5008 in einer Fluchtlinie
% ------------------------------------------------------------------------------
% Abstand zwischen Nummerierung und Überschrift definieren
% > Schön wäre hier die dynamische Berechnung des Abstandes in Abhängigkeit
% > der Verschachtelungstiefe des Inhaltsverzeichnisses
\newcommand{\headingSpace}{1.5cm}
% Abschnittsüberschriften im selben Stil wie beim Inhaltsverzeichnis einrücken
\renewcommand*{\othersectionlevelsformat}[3]{
\makebox[\headingSpace][l]{#3\autodot}
}
% Für die Einrückung wird das Paket tocloft benötigt
%\cftsetindents{chapter}{0.0cm}{\headingSpace}
\cftsetindents{section}{0.0cm}{\headingSpace}
\cftsetindents{subsection}{0.0cm}{\headingSpace}
\cftsetindents{subsubsection}{0.0cm}{\headingSpace}
\cftsetindents{figure}{0.0cm}{\headingSpace}
\cftsetindents{table}{0.0cm}{\headingSpace}
% Allgemeines
% ------------------------------------------------------------------------------
\setstretch{1.15} % Zeilenabstand 1.15 Zeilen
\frenchspacing % erzeugt ein wenig mehr Platz hinter einem Punkt
\renewcommand{\rmdefault}{phv} % Arial als Std Schriftart
\renewcommand{\sfdefault}{phv}
% Schusterjungen und Hurenkinder vermeiden
\clubpenalty = 10000
\widowpenalty = 10000
\displaywidowpenalty = 10000
% Quellcode-Ausgabe formatieren
\lstset{numbers=left, numberstyle=\tiny, numbersep=5pt, breaklines=true}
\lstset{emph={square}, emphstyle=\color{red}, emph={[2]root,base}, emphstyle={[2]\color{blue}}}
\counterwithout{footnote}{section} % Fußnoten fortlaufend durchnummerieren
\setcounter{tocdepth}{3} % im Inhaltsverzeichnis werden die Kapitel bis zum Level der subsubsection übernommen
\setcounter{secnumdepth}{3} % Kapitel bis zum Level der subsubsection werden nummeriert
% Aufzählungen anpassen
\renewcommand{\labelenumi}{\arabic{enumi}.}
\renewcommand{\labelenumii}{\arabic{enumi}.\arabic{enumii}.}
\renewcommand{\labelenumiii}{\arabic{enumi}.\arabic{enumii}.\arabic{enumiii}}
% Tabellenfärbung:
\definecolor{heading}{rgb}{0.64,0.78,0.86}
\definecolor{odd}{rgb}{0.9,0.9,0.9}

View file

@ -1,3 +0,0 @@
% Trennvorschläge im Text werden mit \" angegeben
% untrennbare Wörter und Ausnahmen von der normalen Trennung können in dieser
% Datei mittels \hyphenation definiert werden

File diff suppressed because it is too large Load diff

View file

@ -1,35 +0,0 @@
% !TEX root = Projektdokumentation.tex
\section{Anhang}
\subsection{Implementierungsbeispiele}
\label{app:CodeSchichten}
\subsubsection{Frontend-Schicht: Angular Component}
\label{app:FrontendComponent}
\lstinputlisting[language=C, caption={Angular TypeScript Component - Coinflip Game}]{Listings/CoinflipComponent.ts}
\clearpage
\subsubsection{Controller-Schicht: Spring Boot REST Controller}
\label{app:ControllerSchicht}
\lstinputlisting[language=java, caption={Spring Boot REST Controller - Coinflip}]{Listings/CoinflipController.java}
\clearpage
\subsubsection{Service-Schicht: Business Logic}
\label{app:ServiceSchicht}
\lstinputlisting[language=java, caption={Service-Klasse mit Geschäftslogik - Coinflip}]{Listings/CoinflipService.java}
\clearpage
\subsubsection{Persistierung-Schicht: JPA Entity}
\label{app:PersistierungSchicht}
\lstinputlisting[language=java, caption={JPA Entity - Benutzer}]{Listings/UserEntity.java}
\clearpage
\subsubsection{Konfiguration: Application Properties}
\label{app:Konfiguration}
\lstinputlisting[caption={Spring Boot Anwendungskonfiguration}]{Listings/application.properties}
\clearpage

View file

@ -1,15 +0,0 @@
\subsection{Benutzerdokumentation}
\label{app:BenutzerDoku}
Ausschnitt aus der Benutzerdokumentation:
\begin{table}[htb]
\begin{tabularx}{\textwidth}{cXX}
\rowcolor{heading}\textbf{Symbol} & \textbf{Bedeutung global} & \textbf{Bedeutung einzeln} \\
\includegraphicstotab[]{weather-clear.png} & Alle Module weisen den gleichen Stand auf. & Das Modul ist auf dem gleichen Stand wie das Modul auf der vorherigen Umgebung. \\
\rowcolor{odd}\includegraphicstotab[]{weather-clear-night.png} & Es existieren keine Module (fachlich nicht möglich). & Weder auf der aktuellen noch auf der vorherigen Umgebung sind Module angelegt. Es kann also auch nichts übertragen werden. \\
\includegraphicstotab[]{weather-few-clouds-night.png} & Ein Modul muss durch das Übertragen von der vorherigen Umgebung erstellt werden. & Das Modul der vorherigen Umgebung kann übertragen werden, auf dieser Umgebung ist noch kein Modul vorhanden. \\
\rowcolor{odd}\includegraphicstotab[]{weather-few-clouds.png} & Auf einer vorherigen Umgebung gibt es ein Modul, welches übertragen werden kann, um das nächste zu aktualisieren. & Das Modul der vorherigen Umgebung kann übertragen werden um dieses zu aktualisieren. \\
\includegraphicstotab[]{weather-storm.png} & Ein Modul auf einer Umgebung wurde entgegen des Entwicklungsprozesses gespeichert. & Das aktuelle Modul ist neuer als das Modul auf der vorherigen Umgebung oder die vorherige Umgebung wurde übersprungen. \\
\end{tabularx}
\end{table}

View file

@ -1,7 +0,0 @@
\subsection{Entwicklerdokumentation}
\label{app:Doc}
\begin{center}
\includegraphics[page=1, width=0.9\textwidth]{doc.pdf}
\includegraphics[page=2, width=0.9\textwidth]{doc.pdf}
\end{center}

View file

@ -1,19 +0,0 @@
\subsection{Oberflächenentwürfe}
\label{app:Entwuerfe}
\begin{figure}[htb]
\centering
\includegraphicsKeepAspectRatio{MockupModules.pdf}{0.7}
\caption{Liste der Module mit Filtermöglichkeiten}
\end{figure}
\begin{figure}[htb]
\centering
\includegraphicsKeepAspectRatio{MockupModul.pdf}{0.7}
\caption{Anzeige der Übersichtsseite einzelner Module}
\end{figure}
\begin{figure}[htb]
\centering
\includegraphicsKeepAspectRatio{MockupTag.pdf}{0.7}
\caption{Anzeige und Filterung der Module nach Tags}
\end{figure}

View file

@ -1,22 +0,0 @@
\subsection{Lastenheft (Auszug)}
\label{app:Lastenheft}
Im folgenden Auszug des Lastenheftes werden die Anforderungen definiert, die an die neu entwickelte Anwendung gestellt werden.
Die Anwendung muss folgende Anforderungen erfüllen:
\begin{enumerate}[itemsep=0em,partopsep=0em,parsep=0em,topsep=0em]
\item Verarbeitung der empfangenen Daten von \ac{IX}
\begin{enumerate}
\item Die Anwendung muss Produkt- und Nutzerdaten von der \ac{IX}-\ac{API} abrufen können.
\item Die geholten Daten sollen temporär zwischengespeichert werden, um den Import zu \ac{M2} von der Datenbeschaffung abzukapseln
\item Die gespeicherten Daten werden getrennt von der \ac{IX}-\ac{API} an die \ac{M2} Import-Funktionen gegeben und so in das \ac{M2} System integriert
\end{enumerate}
\item Sonstige Anforderungen
\begin{enumerate}
\item Die Anwendung muss ohne das Installieren einer zusätzlichen Software über das Terminal erreichbar sein.
\item Die Import der Daten muss jede Nacht \bzw nach jedem Fehlschlag automatisch aktualisiert werden.
\item Die Anwendung soll jederzeit erreichbar sein.
\item Da sich die Entwickler auf die Anwendung verlassen, muss diese korrekte Daten liefern und darf keinen Interpretationsspielraum lassen.
\item Die Anwendung muss so flexibel sein, dass sie bei Änderungen im Entwicklungsprozess einfach angepasst werden kann.
\end{enumerate}
\end{enumerate}

View file

@ -1,52 +0,0 @@
\subsection{Pflichtenheft (Auszug)}
\label{app:Pflichtenheft}
\subsubsection*{Zielbestimmung}
\begin{enumerate}[itemsep=0em,partopsep=0em,parsep=0em,topsep=0em]
\item Musskriterien % Wikipedia: für das Produkt unabdingbare Leistungen, die in jedem Fall erfüllt werden müssen
\begin{enumerate}
\item Modul-Liste: Zeigt eine filterbare Liste der Module mit den dazugehörigen Kerninformationen sowie Symbolen zur Einhaltung des Entwicklungsprozesses an
\begin{itemize}
\item In der Liste wird der Name, die Bibliothek und Daten zum Source und Kompilat eines Moduls angezeigt.
\item Ebenfalls wird der Status des Moduls hinsichtlich Source und Kompilat angezeigt. Dazu gibt es unterschiedliche Status-Zeichen, welche symbolisieren in wie weit der Entwicklungsprozess eingehalten wurde \bzw welche Schritte als nächstes getan werden müssen. So gibt es \zB Zeichen für das Einhalten oder Verletzen des Prozesses oder den Hinweis auf den nächsten zu tätigenden Schritt.
\item Weiterhin werden die Benutzer und Zeitpunkte der aktuellen Version der Sourcen und Kompilate angezeigt. Dazu kann vorher ausgewählt werden, von welcher Umgebung diese Daten gelesen werden sollen.
\item Es kann eine Filterung nach allen angezeigten Daten vorgenommen werden. Die Daten zu den Sourcen sind historisiert. Durch die Filterung ist es möglich, auch Module zu finden, die in der Zwischenzeit schon von einem anderen Benutzer editiert wurden.
\end{itemize}
\item Tag-Liste: Bietet die Möglichkeit die Module anhand von Tags zu filtern.
\begin{itemize}
\item Es sollen die Tags angezeigt werden, nach denen bereits gefiltert wird und die, die noch der Filterung hinzugefügt werden könnten, ohne dass die Ergebnisliste leer wird.
\item Zusätzlich sollen die Module angezeigt werden, die den Filterkriterien entsprechen. Sollten die Filterkriterien leer sein, werden nur die Module angezeigt, welche mit einem Tag versehen sind.
\end{itemize}
\item Import der Moduldaten aus einer bereitgestellten \acs{CSV}-Datei
\begin{itemize}
\item Es wird täglich eine Datei mit den Daten der aktuellen Module erstellt. Diese Datei wird (durch einen Cronjob) automatisch nachts importiert.
\item Dabei wird für jedes importierte Modul ein Zeitstempel aktualisiert, damit festgestellt werden kann, wenn ein Modul gelöscht wurde.
\item Die Datei enthält die Namen der Umgebung, der Bibliothek und des Moduls, den Programmtyp, den Benutzer und Zeitpunkt des Sourcecodes sowie des Kompilats und den Hash des Sourcecodes.
\item Sollte sich ein Modul verändert haben, werden die entsprechenden Daten in der Datenbank aktualisiert. Die Veränderungen am Source werden dabei aber nicht ersetzt, sondern historisiert.
\end{itemize}
\item Import der Informationen aus \ac{SVN}. Durch einen \gqq{post-commit-hook} wird nach jedem Einchecken eines Moduls ein \acs{PHP}-Script auf der Konsole aufgerufen, welches die Informationen, die vom \ac{SVN}-Kommandozeilentool geliefert werden, an \acs{NatInfo} übergibt.
\item Parsen der Sourcen
\begin{itemize}
\item Die Sourcen der Entwicklungsumgebung werden nach Tags, Links zu Artikeln im Wiki und Programmbeschreibungen durchsucht.
\item Diese Daten werden dann entsprechend angelegt, aktualisiert oder nicht mehr gesetzte Tags/Wikiartikel entfernt.
\end{itemize}
\item Sonstiges
\begin{itemize}
\item Das Programm läuft als Webanwendung im Intranet.
\item Die Anwendung soll möglichst leicht erweiterbar sein und auch von anderen Entwicklungsprozessen ausgehen können.
\item Eine Konfiguration soll möglichst in zentralen Konfigurationsdateien erfolgen.
\end{itemize}
\end{enumerate}
\end{enumerate}
\subsubsection*{Produkteinsatz}
\begin{enumerate}[itemsep=0em,partopsep=0em,parsep=0em,topsep=0em]
\item{Anwendungsbereiche\\
Die Webanwendung dient als Anlaufstelle für die Entwicklung. Dort sind alle Informationen für die Module an einer Stelle gesammelt. Vorher getrennte Anwendungen werden ersetzt \bzw verlinkt.}
\item{Zielgruppen\\
\NI wird lediglich von den \ac{Natural}-Entwicklern in der EDV-Abteilung genutzt.}
\item{Betriebsbedingungen\\ % Wikipedia: physikalische Umgebung des Systems, tägliche Betriebszeit, ständige Beobachtung des Systems durch Bediener oder unbeaufsichtigter Betrieb
Die nötigen Betriebsbedingungen, also der Webserver, die Datenbank, die Versionsverwaltung, das Wiki und der nächtliche Export sind bereits vorhanden und konfiguriert. Durch einen täglichen Cronjob werden entsprechende Daten aktualisiert, die Webanwendung ist jederzeit aus dem Intranet heraus erreichbar.}
\end{enumerate}

View file

@ -1,28 +0,0 @@
\subsection{Verwendete Ressourcen}
\label{app:Ressourcen}
Hardware
\begin{itemize}
\item Büroarbeitsplatz (Schreibtisch, ergonomischer Stuhl)
\item Fujitsu Lifebook U-Series - Notebook
\item Peripheriegeräte für Notebook (Tastatur, Maus)
\end{itemize}
Software
\begin{itemize}
\item Debian Derivat Ubuntu 18.04 - Betriebssystem
\item JetBrains PHPStorm - Entwicklungsumgebung \ac{PHP}
\item git - Verteilte Versionsverwaltung
\item Docker - Open Source Management von virtuellen Maschinen (Containerbasiert)
\item mySQL - Open Source relationelles Datenbank Management System
\item Composer - Open Source Package Management System auf Anwendungsebene für \ac{PHP}
\item Magento 2 Community Edition - Open Source \ac{eCommerce} Platform
\item TeXify IDEA - Open Source Plugin für PHPStorm als Distribution des Textsatzsystems TeX
\item PHPUnit - Framework zur Durchführung von Unit-Tests
\item LaTeX-Vorlage zur IHK-Projektdokumentation für Fachinformatiker Anwendungsentwicklung von Stefan Macke - \url{http://fiae.link/LaTeXVorlageFIAE}
\end{itemize}
Personal
\begin{itemize}
\item Mitarbeiter Softwareentwicklung der \ac{NSD} - Festlegung der Anforderungen, Abnahme des Projektes \& Review des Codes
\item Auszubildender Fachinformatiker als Entwickler - Umsetzung des Projektes
\end{itemize}

View file

@ -1,14 +0,0 @@
\subsection{Screenshots der Anwendung}
\label{Screenshots}
\begin{figure}[htb]
\centering
\includegraphicsKeepAspectRatio{tagliste.pdf}{1}
\caption{Anzeige und Filterung der Module nach Tags}
\end{figure}
\clearpage
\begin{figure}[htb]
\centering
\includegraphicsKeepAspectRatio{modulliste.pdf}{1}
\caption{Liste der Module mit Filtermöglichkeiten}
\end{figure}
\clearpage

View file

@ -1,9 +0,0 @@
\subsection{Testfall und sein Aufruf auf der Konsole}
\label{app:Test}
\lstinputlisting[language=php, caption={Testfall in PHP}]{Listings/tests.php}
\clearpage
\begin{figure}[htb]
\centering
\includegraphicsKeepAspectRatio{testcase.jpg}{1}
\caption{Aufruf des Testfalls auf der Konsole}
\end{figure}

View file

@ -1,4 +0,0 @@
% Abkürzungen
\newcommand{\Versis}{\textsc{Versis}\xspace}
\newcommand{\NI}{NatInfo\xspace}
\newcommand{\AO}{\textsc{Alte Oldenburger} Krankenversicherung\xspace}

View file

@ -1,92 +0,0 @@
% This file was created with JabRef 2.4.2.
% Encoding: UTF8
@book{Rohrer2011,
address = {Solingen},
edition = {5.},
title = {Clevere Tipps für die Projektarbeit - {IT-Berufe:} Abschlussprüfung Teil A},
isbn = {3882347538},
url = {http://fiae.link/ClevereTippsFuerDieProjektarbeit},
publisher = {{U-Form-Verlag}},
author = {Rohrer, Anselm and Sedlacek, Ramona},
year = {2011}
}
@misc{Bundesgesetzblatt48,
title = {Verordnung über die Berufsausbildung im Bereich der Informations- und Telekommunikationstechnik},
url = {http://fiae.link/VerordnungITBerufe},
author = {{Regierung der Bundesrepublik Deutschland}},
month = jul,
year = {1997},
pages = {1741--1799}
}
@techreport{BMBF2000,
address = {Bonn},
type = {Abschlussbericht},
title = {Umsetzungshilfen für die neue Prüfungsstruktur der {IT-Berufe}},
shorttitle = {{BMBF2000}},
url = {http://fiae.link/UmsetzungshilfenITBerufe},
author = {{Bundesministerium für Bildung und Forschung}},
institution = {{Bundesministerium für Bildung und Forschung}},
month = jul,
year = {2000},
pages = {476}
}
@techreport{Grashorn2010,
address = {Vechta},
type = {Dokumentation zur Projektarbeit},
title = {Entwicklung von NatInfo -- Webbasiertes Tool zur Unterstützung der Entwickler},
author = {Dirk Grashorn},
institution = {{Alte Oldenburger Krankenversicherung AG}},
month = apr,
year = {2010},
}
@misc{MerkblattIHK,
address = {Oldenburg},
title = {Merkblatt zur Abschlussprüfung der IT-Berufe},
author = {{IHK Oldenburg}},
institution = {{Oldenburgische Industrie- und Handelskammer}},
month = may,
year = {2006},
url = {http://fiae.link/MerkblattDokuOldenburg},
}
@misc{BewertungsmatrikIHK,
address = {Darmstadt},
title = {Bewertungsmatrix für Fachinformatiker/innen Anwendungsentwicklung},
author = {{IHK Darmstadt}},
institution = {{IHK Darmstadt Rhein Main Neckar}},
month = mar,
year = {2011},
url = {http://fiae.link/BewertungsmatrixDokuDarmstadt},
}
@misc{ISO9126,
title = {{Software-Engineering} Qualität von {Software-Produkten} Teil 1: Qualitätsmodell},
author = {{ISO/IEC 9126-1}},
month = jun,
year = {2001}
}
@BOOKLET{phpDoc,
title = {{phpDocumentor-Website}},
author = {{phpdoc.org}},
lastchecked = {20.04.2010},
year = {2010},
owner = {grashorn},
timestamp = {2010.04.22},
url = {http://www.phpdoc.org/}
}
@BOOKLET{Symfony,
title = {Symfony - Open-Source PHP Web Framework},
author = {{Sensio Labs}},
lastchecked = {20.04.2010},
year = {2010},
owner = {grashorn},
timestamp = {2010.04.22},
url = {http://www.symfony-project.org/}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -1,27 +0,0 @@
% !TEX root = Projektdokumentation.tex
\begin{titlepage}
\begin{center}
\includegraphics[scale=0.25]{hitec-logo.png}\\[1ex]
\Large{\ausbildungsberuf}\\
\LARGE{\betreff}\\[4ex]
\huge{\textbf{\titel}}\\[1.5ex]
\Large{\textbf{\untertitel}}\\[4ex]
\normalsize
Projektzeitraum: \projektZeitraum\\[3em]
\textbf{Projektteilnehmer:}\\
Constantin Simonis\\
Phan Huy Tran\\
Jan-Marlon Leibl\\
Jan Klattenhoff\\
Lea Ziemke\\
\textbf{Projektaufsicht:}\\
Katrin Deeken\\
Bernd Heidemann\\
\end{center}
\end{titlepage}

View file

@ -1,16 +0,0 @@
% !TEX root = Projektdokumentation.tex
\input{Inhalt/Einleitung}
\input{Inhalt/Vorbereitung.tex}
\input{Inhalt/Projektarchitektur}
\input{Inhalt/CI}
\input{Inhalt/Auth.tex}
\input{Inhalt/Dice.tex}
\input{Inhalt/Slots.tex}
\input{Inhalt/Coinflip.tex}
\input{Inhalt/Blackjack.tex}
\input{Inhalt/Lootboxes.tex}
\input{Inhalt/Deployment.tex}
\input{Inhalt/Wirtschaftlichebetrachtung.tex}
\input{Inhalt/Abschluss.tex}

View file

@ -1,12 +0,0 @@
% !TEX root = ../Projektdokumentation.tex
\section{Abschluss}
\subsection{Projektziel}
Im Rahmen der Soll-Analyse konnten wesentliche Bestandteile des Projekts erfolgreich umgesetzt werden. Aufgrund zeitlicher Einschränkungen sowie unvorhergesehener technischer Herausforderungen war es jedoch nicht möglich, sämtliche ursprünglich geplanten Funktionen vollständig zu realisieren. Während der Entwicklungsphase wurde ein besonderer Schwerpunkt auf die technischen Grundlagen gelegt, insbesondere auf die sichere Authentifizierung, die serverseitige Logik sowie die Bereitstellung einer stabilen Infrastruktur. Monetarisierungsstrategien und benutzerorientierte Aspekte wurden in dieser Phase hingegen nur am Rande berücksichtigt.
Im Verlauf der Ausarbeitung wurde zudem deutlich, dass die Umsetzung einzelner Spiele deutlich komplexer ist als zunächst angenommen. Aus diesem Grund wurde die Entwicklung auf den Kern des Spielkonzepts reduziert, wohingegen begleitende Elemente wie soziale Funktionen oder ein vollständig responsives Design vorerst nicht weiter ausgearbeitet wurden. Im weiteren Projektverlauf wurde deutlich, dass die Implementierung von Poker und Plinko nicht mehr im geplanten Zeitrahmen realisiert werden konnte. Insbesondere die Entwicklung der Authentifizierungsmechanismen sowie das Deployment der Anwendung erforderten einen höheren Aufwand als erwartet.
\subsection{Fazit}
Die Zusammenarbeit im Team kann als grundsätzlich positiv bewertet werden. Es sei besonders hervorgehoben, dass eine offene und funktionierende Kommunikation existierte, die es ermöglichte, Herausforderungen gemeinsam zu bewältigen und Entscheidungen transparent zu treffen. Gleichzeitig wurde jedoch Optimierungspotenzial in der Organisation und Strukturierung der Arbeitsprozesse aufgezeigt. Das Projektverhalten zeigte phasenweise Unkoordiniertheit und chaotische Züge, was die Effizienz beeinträchtigte. Das Ticket-Handling hätte klarer und konsequenter erfolgen müssen, um Aufgaben besser zu priorisieren und den Überblick über den Projektfortschritt zu wahren. Trotz der zuvor genannten strukturellen Schwächen war eine gute inhaltliche Zusammenarbeit innerhalb des Teams zu verzeichnen, in deren Folge bedeutende Meilensteine erreicht werden konnten.
Trotzdem war das Projekt von erheblicher Frustration geprägt, deren Hauptursache die als unzureichend empfundene Technologie, insbesondere Java, war. Gepaart mit Mängeln im Projektmanagement, wie zu spät begonnener Dokumentation und ineffektiven Retrospektiven, führte der massive Zeitdruck zu einem unbefriedigenden Ergebnis. Das Gesamterlebnis gipfelt in dem Gefühl, letztlich „garnichts“ erreicht zu haben, und dem nachdrücklichen Wunsch, Java zukünftig zu meiden

View file

@ -1,23 +0,0 @@
\section{Authentifizierung}
\label{sec:Authentifizierung}
Die Authentifizierung gegenüber der \acs{API} erfolgt über einen \acs{JWT}-Token, der dem Frontend nach Erfolgreicher Authentifizierung übergeben wird.
Authentifizierung läuft über zwei verschiedene Wege ab:
\subsection{Registrierung mit username/password} Der Nutzer füllt ein Registrierungs-Formular aus, welches die Anmeldedaten an die \acs{API} sendet. Diese validitert die Anmeldedaten und legt bei Erfolg einen neuen Nutzer an. Anschließend wird eine E-Mail-Verifizierungs-Mail gesendet. Bis der Link in der Verifizierungs-Mail nicht angeklickt wurde, ist der Nutzer nicht aktiv und kann sich nicht anmelden. Nach dem Klick auf den Link wird der Nutzer aktiviert und kann sich anmelden.
\subsection{Login mit username/password} Der Nutzer füllt ein Anmelde-Formular, welches die Anmeldedaten an die \acs{API} sendet. Diese prüft die Anmeldedaten und gibt bei Erfolg einen \acs{JWT}-Token zurück. Falls kein Nutzer mit den Anmeldedaten existiert, wird der Nutzer aufgefordert einen Account zu erstellen.
\begin{figure}
\centering
\includegraphics[width=0.3\textwidth]{login.png}
\caption{Login-Formular der Anwendung}
\label{fig:login}
\end{figure}
\subsection{Login über Oauth (Open Authorization)} Der Nutzer meldet sich mit einem Oauth-Provider an, in unserem Fall Google oder Github. Das Backend leitet den Nutzer zum Oauth-Provider weiter, der die Anmeldedaten prüft und bei Erfolg den Nutzer auf die Applikation weiterleitet und einen Authorization-Code zurück gibt. Mit diesem Code holt sich die \acs{API} einen \acs{JWT} vom jeweiligen Provider und holt sich Nutzer-Informationen. Mit diesen wird dann ein existierender Nutzer eingeloggt, oder registriert falls der Nutzer noch kein Konto hatte. Anschließend wird von der \acs{API} ein \acs{JWT} generiert und an das Frontend weitergegeben.
\begin{figure}
\centering
\includegraphics[width=0.45\textwidth]{oauth.png}
\caption{OAuth-Authentifizierungsablauf}
\label{fig:oauth}
\end{figure}

View file

@ -1,52 +0,0 @@
\section{Blackjack}
\subsection{Was ist Blackjack?}
Blackjack ist eines der populärsten Kartenspiele in Casinos weltweit und wurde als vollständiges Spiel in die Casino-Plattform integriert. Das Ziel des Spiels ist es, mit den eigenen Karten einen Wert von 21 zu erreichen oder näher an 21 heranzukommen als der Dealer, ohne dabei über 21 zu gehen (Bust). Das Spiel kombiniert Strategie und Glück, da Spieler Entscheidungen über ihre Spielzüge treffen müssen, während sie gegen einen automatisierten Dealer antreten.
Die Implementierung folgt den klassischen Blackjack-Regeln: Zahlenkarten haben ihren Nennwert, Bildkarten (König, Dame, Bube) zählen 10 Punkte, und Asse können je nach Situation als 1 oder 11 gewertet werden. Ein "Blackjack" (21 mit den ersten beiden Karten) zahlt mit einem Bonus von 1,5x des Einsatzes aus, während reguläre Gewinne den doppelten Einsatz zurückgeben.
\subsubsection{Kartensystem und Deck-Verwaltung}
Das Blackjack-System verwendet ein vollständiges 52-Karten-Deck mit vier Farben (Herz, Karo, Kreuz, Pik) und 13 Rängen (2-10, Bube, Dame, König, Ass). Die Karten werden in der Datenbank als separate Entitäten gespeichert, wobei jede Karte einem Typ zugeordnet ist: DECK (noch im Stapel), PLAYER (Spielerhand) oder DEALER (Dealerhand).
Die DeckService-Klasse verwaltet die Kartenerstellung und -verteilung. Bei Spielbeginn wird ein neues, gemischtes Deck erstellt und die Karten werden entsprechend den Spielregeln verteilt: Der Spieler erhält zwei Karten, der Dealer erhält eine Karte. Die Zufallsmischung erfolgt über java.util.Random, um eine faire Kartenverteilung zu gewährleisten.
\subsubsection{Spielzustände und Ablauf}
Das Spiel durchläuft verschiedene definierte Zustände:
\begin{itemize}
\item \textbf{IN\_PROGRESS:} Das Spiel läuft aktiv, der Spieler kann Aktionen ausführen
\item \textbf{PLAYER\_BLACKJACK:} Der Spieler hat einen natürlichen Blackjack (21 mit zwei Karten)
\item \textbf{PLAYER\_WON:} Der Spieler hat gewonnen ohne Blackjack
\item \textbf{PLAYER\_LOST:} Der Spieler hat verloren oder sich überkauft
\item \textbf{DRAW:} Unentschieden zwischen Spieler und Dealer
\end{itemize}
Der Spielablauf gestaltet sich folgendermaßen:
\begin{enumerate}
\item Spieler startet das Spiel mit einem Einsatz
\item Austeilung der Anfangskarten (2 für Spieler, 1 für Dealer)
\item Spieler trifft Entscheidungen: Hit (weitere Karte), Stand (keine weitere Karte), Double Down (Einsatz verdoppeln und eine Karte)
\item Bei Stand aktiviert sich die Dealer-Logik: Dealer zieht Karten bis mindestens 17 erreicht sind
\item Gewinnermittlung und Auszahlung entsprechend des Ergebnisses
\end{enumerate}
\subsubsection{Handwertberechnung}
Die Berechnung des Handwerts stellt den Kern der Spiellogik dar. Zunächst werden die Grundwerte aller Karten addiert, wobei Asse initial mit 11 bewertet werden. Falls die Summe 21 übersteigt und Asse vorhanden sind, werden diese nacheinander von 11 auf 1 reduziert, bis entweder die Summe unter 22 liegt oder keine Asse mehr als 11 gewertet werden können.
Diese flexible Ass-Bewertung ermöglicht optimale Strategien: Ein Ass kann als "Soft" (11) oder "Hard" (1) gewertet werden, abhängig von den anderen Karten in der Hand. Die Implementierung sorgt automatisch für die bestmögliche Bewertung der Spielerhand.
\subsubsection{Wettsystem und Auszahlungen}
Das Blackjack-Spiel ist vollständig in das Wettsystem der Plattform integriert. Der Einsatz wird zu Spielbeginn vom Spielerkonto abgebucht und bei Gewinn entsprechend der Auszahlungsregeln gutgeschrieben:
\begin{itemize}
\item \textbf{Blackjack:} 1,5x Einsatz plus ursprünglicher Einsatz zurück
\item \textbf{Regulärer Gewinn:} 2x Einsatz (Verdopplung)
\item \textbf{Unentschieden:} Ursprünglicher Einsatz wird zurückerstattet
\item \textbf{Niederlage:} Kein Gewinn, Einsatz verloren
\end{itemize}
Die Double-Down-Funktion verdoppelt den ursprünglichen Einsatz und gibt dem Spieler genau eine weitere Karte, danach ist das Spiel automatisch beendet. Diese Aktion ist nur mit den ersten beiden Karten möglich und erfordert ausreichendes Guthaben für den zusätzlichen Einsatz.
\subsubsection{Frontend-Integration}
Das Frontend bietet eine vollständig animierte Blackjack-Erfahrung mit visueller Kartenrepräsentation und intuitiver Benutzeroberfläche. Die Spielerkarten werden offen dargestellt, während die zweite Dealer-Karte bis zum Spielende verdeckt bleibt. Sound-Effekte und Animationen verstärken die Spielerfahrung.
Die Zustandssynchronisation zwischen Frontend und Backend erfolgt über reactive Programmierung mit Angular Signals, wodurch Änderungen in Echtzeit dargestellt werden. Fehlerbehandlung sorgt für robuste Spielzustände auch bei Netzwerkproblemen, und die Benutzeroberfläche passt sich dynamisch an verschiedene Spielsituationen an.

View file

@ -1,53 +0,0 @@
% !TEX root = ../Projektdokumentation.tex
\section{Continuous Integration}
\label{sec:CI}
Das Projekt verwendet Gitea Actions\footnote{Gitea Actions - \url{https://docs.gitea.com/usage/actions/overview}} als \ac{CI/CD}-Pipeline, welche vollständig kompatibel mit GitHub Actions ist.
Entsprechend den Qualitätsanforderungen soll eine hohe Code-Qualität durch automatisierte Tests gewährleistet werden.
\subsection{Aufbau der CI-Pipeline}
\label{sec:ci-pipeline}
Die Haupt-\ac{CI}-Pipeline wird durch die Datei \Datei{ci.yml} definiert und bei Pull Requests ausgelöst. Aufgrund der separaten Frontend- und Backend-Komponenten
wurde eine \Fachbegriff{Change Detection} implementiert, welche nur relevante Tests für geänderte Bereiche ausführt.
Ein initialer Job identifiziert geänderte Dateien und ermöglicht eine selektive Ausführung:
\begin{itemize}
\item Backend-Änderungen: \Datei{backend/**}
\item Frontend-Änderungen: \Datei{frontend/**}
\item Workflow-Änderungen: \Datei{.gitea/workflows/**}
\end{itemize}
\subsubsection{Backend-Qualitätssicherung}
Für Backend-Änderungen werden folgende Prüfungen durchgeführt:
\begin{itemize}
\item \textbf{Unit Tests:} Ausführung mit \Eingabe{./gradlew test} in OpenJDK 23 Container
\item \textbf{Checkstyle:} Code-Style-Validierung mit Caching-Mechanismus
\item \textbf{Docker Build:} Überprüfung der Build-Funktionalität
\end{itemize}
\subsubsection{Frontend-Qualitätssicherung}
Für Frontend-Änderungen wird eine umfassende Testsuite ausgeführt:
\begin{itemize}
\item \textbf{ESLint:} Code-Qualitätsprüfung mit \Eingabe{bun run lint}
\item \textbf{Prettier:} Code-Formatierungsvalidierung
\item \textbf{Build-Test:} Produktions-Build-Validierung mit \Eingabe{bun run build}
\item \textbf{Playwright \ac{E2E} Tests:} End-to-End-Tests mit automatischem Backend-Start
\item \textbf{Docker Build:} Validierung der Container-Erstellung
\end{itemize}
\subsection{Release-Management}
\label{sec:release-pipeline}
Das Release-Management erfolgt automatisiert durch die \Datei{release.yml} Pipeline bei Pushes auf den \texttt{main}-Branch.
Die Implementierung folgt \Fachbegriff{Semantic Versioning}\footnote{Semantic Versioning - \url{https://semver.org/}} und \Fachbegriff{Conventional Commits}\footnote{Conventional Commits - \url{https://www.conventionalcommits.org/}}.
Die Release-Pipeline umfasst:
\begin{enumerate}
\item \textbf{Semantic Release:} Automatische Versionierung basierend auf Commit-Nachrichten
\item \textbf{Docker Image Build:} Parallele Erstellung von Backend- und Frontend-Images
\item \textbf{Registry Push:} Upload zur privaten Gitea Docker Registry
\end{enumerate}
Die \ac{CI/CD}-Pipeline implementiert Performance-Optimierungen wie intelligentes Caching, Concurrency Control und selektive Job-Ausführung.
Diese Automatisierung gewährleistet eine hohe Software-Qualität bei effizienten Entwicklungsprozessen.

View file

@ -1,41 +0,0 @@
\section{Coinflip}
\subsection{Was ist Coinflip?}
Das Münzwurf-Spiel 'Coinflip' ist ein klassisches Glücksspiel, das in seiner digitalen Umsetzung den traditionellen Münzwurf simuliert. Das Spiel basiert auf dem einfachen Prinzip einer Münze mit zwei Seiten: Kopf und Zahl. Spieler setzen auf eine der beiden Seiten und haben eine 50\%-ige Gewinnchance. Die Einfachheit des Spiels macht es zu einem idealen Einstiegsspiel für neue Nutzer der Casino-Plattform.
Im Gegensatz zu komplexeren Spielen wie Dice bietet Coinflip eine feste Gewinnwahrscheinlichkeit von 50\% und einen konstanten Multiplikator von 2x. Dies bedeutet, dass Spieler bei einem Gewinn ihren Einsatz verdoppeln, während sie bei einer Niederlage ihren gesamten Einsatz verlieren.
\subsubsection{Zufallszahlengenerierung}
Die Implementierung verwendet die Standardklasse java.util.Random zur Generierung des Münzwurfs.
Die Zufallsgenerierung erzeugt einen booleschen Wert, der anschließend einer der beiden Münzseiten zugeordnet wird.
Diese binäre Entscheidung gewährleistet die faire 50:50-Verteilung, die für ein authentisches Münzwurf-Erlebnis erforderlich ist.
\subsubsection{Spielablauf und Datenfluss}
Der Spielablauf von Coinflip folgt einem strukturierten Datenfluss zwischen Frontend und Backend. Der Controller empfängt die Spielanfrage mit folgenden Parametern:
\begin{itemize}
\item \textbf{Einsatz:} Der gesetzte Münzbetrag.
\item \textbf{Gewählte Seite:} Die vom Spieler gewählte Münzseite (Kopf oder Zahl).
\end{itemize}
Nach dem Erhalt der Anfrage führt der Controller eine Guthabenprüfung durch. Bei ausreichendem Guthaben wird die Anfrage an die Service-Schicht weitergeleitet, andernfalls wird eine entsprechende Fehlermeldung zurückgegeben.
Die Service-Klasse verarbeitet die Spiellogik in folgender Reihenfolge:
\begin{enumerate}
\item Abbuchung des Einsatzes vom Spielerkonto.
\item Generierung des zufälligen Münzwurfs (Kopf oder Zahl).
\item Vergleich zwischen gewählter Seite und Wurfergebnis.
\item Bei einem Gewinn: Gutschrift des doppelten Einsatzes auf das Spielerkonto.
\item Rückgabe des Spielergebnisses an das Frontend.
\end{enumerate}
Das Spielergebnis wird strukturiert an das Frontend übermittelt und enthält:
\begin{itemize}
\item Gewinnstatus (gewonnen/verloren)
\item Auszahlungsbetrag (bei Gewinn: 2x Einsatz)
\item Geworfene Münzseite
\end{itemize}
\subsubsection{Implementierungsdetails}
Die vollständige Implementierung der Coinflip-Funktionalität umfasst verschiedene Architekturschichten: Das Angular Frontend-Component (siehe \ref{app:FrontendComponent}), den Spring Boot REST Controller (siehe \ref{app:ControllerSchicht}) und die Service-Schicht mit der Geschäftslogik (siehe \ref{app:ServiceSchicht}). Zusätzlich wird die Benutzer-Entity (siehe \ref{app:PersistierungSchicht}) für die Guthaben-Verwaltung verwendet.

View file

@ -1,5 +0,0 @@
\section{Deployment}
\label{sec:Deployment}
Es gibt zwei Server auf denen Instanzen der Applikation laufen.
\subsection{\href{https://casino.simonis.lol/}{Entwicklungsserver}} Auf dem Entwicklungsserver läuft eine Instanz der Applikation, die für die Entwicklung und das Testen von neuen Features genutzt wird. Diese Instanz ist Lokal bei Constantin gehostet und wird durch einen Cloudflare-Tunnel öffentlich zugänglich gemacht.
\subsection{\href{https://trustworthy.casino/}{Produktionsserver}} Auf dem Produktionsserver läuft die finale Version der Applikation, die für die Nutzer zugänglich ist. Diese Instanz ist öffentlich zugänglich und wird von den Nutzern genutzt. Diese Instanz ist auf einem gemieteten Server gehostet. Die Applikation wird durch eine Nginx Reverse-Proxy bereitgestellt, die Anfragen an die \acs{API} und das Frontend weiterleitet und SSL-Zertifikate verwaltet. Die Konfiguration der Anwendung erfolgt über Umgebungsvariablen und Properties-Dateien (siehe \ref{app:Konfiguration}).

View file

@ -1,43 +0,0 @@
\section{Dice}
\subsection{Was ist Dice?}
Das Würfelspiel 'Dice' ist ein originelles Spiel der Casinoplattform Stake.com\footnote{Stake.com ist eine bekannte Online-Glücksspielplattform, die eine Vielzahl von Casinospielen und Sportwetten anbietet.}. Das Spiel dreht sich um einen virtuellen 100-seitigen Würfel,
bei dem Spieler die Parameter ihrer Wette beeinflussen können. Im Kern geht es darum,
einen zuvor festgelegten 'Roll Over'- oder 'Roll Under'-Betrag zu unter- oder überschreiten,
um eine Runde zu gewinnen. Spieler haben die Kontrolle über den Multiplikator und ihre Gewinnchancen:
Durch die Anpassung des Zielwerts können sie das Verhältnis
von Risiko und potenzieller Auszahlung steuern. Ein höherer Multiplikator verspricht zwar größere Gewinne,
reduziert jedoch gleichzeitig die Wahrscheinlichkeit eines erfolgreichen Würfelwurfs.
\subsubsection{Zufallszahlengenerierung}
Zur Generierung des Würfelwurfs verwendet diese Implementierung die Standardklasse java.util.Random.
Sie erzeugt eine pseudo-zufällige Zahl zwischen 1 und 100 (inklusive),
die das Ergebnis des virtuellen 100-seitigen Würfels darstellt.
\subsubsection{Spielablauf und Datenfluss}
Der zentrale Controller steuert den Spielablauf und empfängt die Anfragen vom Frontend. Jede Anfrage enthält die Eckdaten des gewünschten Würfelwurfs:
\begin{itemize}
\item \textbf{Einsatz:} Der gesetzte Münzbetrag.
\item \textbf{Wettart:} Soll der Würfel ``über'' oder ``unter'' einen Wert fallen?
\item \textbf{Zielwert:} Der vom Spieler festgelegte Referenzwert (1-100).
\end{itemize}
Zuerst prüft der Controller das Guthaben des Spielers. Bei unzureichenden Mitteln wird der Vorgang abgelehnt. Andernfalls übergibt er die weitere Ausführung an die Dienstklasse.
Die Dienstklasse übernimmt die eigentliche Logik des Würfelspiels:
\begin{enumerate}
\item Zieht den Einsatz vom Spielerkonto ab.
\item Erzeugt einen zufälligen Würfelwurf (Wert zwischen 1 und 100).
\item Prüft, ob der Wurf die Gewinnbedingung erfüllt (entsprechend Wettart und Zielwert).
\item Berechnet die Gewinnwahrscheinlichkeit und den sich daraus ergebenden Multiplikator.
\item Schreibt bei einem Gewinn den entsprechenden Betrag (Einsatz $\times$ Multiplikator) dem Spielerkonto gut.
\end{enumerate}
Das Ergebnis des Spiels wird an das Frontend zurückgesendet und enthält:
\begin{itemize}
\item Gewinnstatus (gewonnen/verloren)
\item Auszahlungsbetrag
\item Gewürfelten Wert
\end{itemize}

View file

@ -1,57 +0,0 @@
% !TEX root = ../Projektdokumentation.tex
\section{Einleitung}
\label{sec:Einleitung}
Die folgende Projektdokumentation beschreibt die Entwicklung einer innovativen Online-Casino-Plattform für die HiTec GmbH,
ein mittelgroßes IT-Systemhaus mit Sitz in Bremen, das seit über 15 Jahren als IT-Dienstleister tätig und seit einigen Jahren ISO/IEC 27001 zertifiziert ist. Das Kerngeschäft der HiTec GmbH umfasst Entwicklung eigener Softwareprodukte, Consulting, IT-Systembereich sowie Support und Wartung und beschäftigt spezialisierte Mitarbeiter in verschiedenen Abteilungen.
\subsection{Projektumfeld}
\label{sec:Projektumfeld}
Auftraggeber des Projektes ist Herr Hofmann, der Inhaber der HiTec GmbH.
Das Unternehmen möchte sein Produkt-Portfolio in den nächsten zwei Jahren wesentlich erweitern, um weiterhin am Markt zu bestehen und noch stärker zu wachsen. Dazu soll der Bereich der Webanwendungen mit einem neuartigen Produkt beitragen, das über einzigartige Features verfügt.
Das Ziel ist es, das fachliche Know-How des Entwicklungsteams stärker zu nutzen, um als innovatives IT-Unternehmen langfristig am Markt wahrgenommen zu werden und schnell Erträge zu erzielen.
Die technische Umsetzung erfolgt mit den in der HiTec GmbH etablierten Technologien und Projektmanagement-Methoden unter Verwendung agiler Entwicklungsmethoden.
\subsection{Projektziel}
\label{sec:Projektziel}
Ziel des Projektes ist die Entwicklung einer innovativen Online-Casino-Plattform als neuartiges Produkt für das Portfolio der HiTec GmbH.
Die Plattform soll verschiedene Casino-Spiele wie Blackjack, Coinflip, Würfelspiele und Spielautomaten anbieten und durch moderne Features wie Lootboxes und ein umfassendes Benutzermanagement erweitert werden.
Das System soll eine vollständige Transaktionsabwicklung mit Einzahlungsfunktionen und eine sichere Benutzerauthentifizierung über OAuth2-Integration bieten.
Durch den Einstieg in den Entertainment-Bereich soll das bisherige IT-Systemhaus-Portfolio um ein profitables Gaming-Produkt erweitert werden.
Die Fokussierung auf Social Casino Games ermöglicht den Zugang zum wachsenden Markt der Casual-Gamer, während gleichzeitig die strengen Glücksspiellizenzen und regulatorischen Anforderungen umgangen werden können.
Die Monetarisierung erfolgt primär über den Verkauf virtueller Währungen und Premium-Features, was bei vergleichsweise niedrigen Entwicklungskosten eine hohe Gewinnmarge verspricht.
Zusätzliche Einnahmequellen werden durch In-App-Käufe und spezielle Lootbox-Angebote generiert.
Diese Webanwendung soll als hochproduktive und kostenarme Implementierung realisiert werden, um schnell Marktreife zu erlangen und Erträge zu generieren.
\subsection{Projektbegründung}
\label{sec:Projektbegruendung}
Der Online-Gaming- und Casino-Markt zeigt kontinuierliches Wachstum und bietet erhebliches Potenzial für innovative Lösungen.
Durch die Entwicklung einer modernen Casino-Plattform kann die HiTec GmbH eine neue Kundengruppe erschließen und eine Nische im Gaming-Bereich besetzen.
Die Verwendung bewährter Technologien (Java mit Spring-Boot, Angular, Keycloak) minimiert die Entwicklungsrisiken und ermöglicht eine schnelle Markteinführung.
Gleichzeitig demonstriert das Projekt die technische Kompetenz des Unternehmens in der Entwicklung komplexer, interaktiver Webanwendungen.
Ein modularer Aufbau mit verschiedenen Spielen und Features ermöglicht eine schrittweise Erweiterung und Anpassung an Marktanforderungen, was langfristige Wartbarkeit und Skalierbarkeit gewährleistet.
\subsection{Projektschnittstellen}
\label{sec:Projektschnittstellen}
Die Anwendung wird als Full-Stack-Webanwendung mit getrenntem Backend und Frontend entwickelt.
Das Backend basiert auf Java mit Spring-Boot und stellt RESTful APIs für die Spiellogik, Benutzerverwaltung und Transaktionsabwicklung bereit.
Für die Benutzerauthentifizierung wird Keycloak als Identity Provider eingesetzt, der OAuth2-Integration mit externen Anbietern wie Google und GitHub ermöglicht.
Die Frontend-Anwendung wird mit Angular entwickelt und kommuniziert über HTTP-APIs mit dem Backend.
Zur Containerisierung und für das Deployment wird Docker verwendet, was eine konsistente Entwicklungs- und Produktionsumgebung gewährleistet.
Die Projektverwaltung erfolgt mit Jira unter Verwendung der Scrum-Methodik, die in der HiTec GmbH als Standard etabliert ist.
\subsection{Projektabgrenzung}
\label{sec:Projektabgrenzung}
Da der Projektumfang beschränkt ist, konzentriert sich die Entwicklung auf die Kernfunktionalitäten der Casino-Plattform.
Erweiterte Features wie Live-Dealer-Spiele, komplexe Turniere oder Integration mit externen Zahlungsdienstleistern sind nicht Bestandteil dieses Mittelstufenprojekts.
Die Implementierung beschränkt sich auf eine begrenzte Anzahl von Casino-Spielen und grundlegende Benutzerfunktionen, um eine funktionsfähige Proof-of-Concept-Anwendung zu erstellen.

View file

@ -1,78 +0,0 @@
\section{Lootboxes}
\subsection{Was sind Lootboxes?}
Lootboxes sind digitale Belohnungsbehälter, die gegen eine feste Gebühr geöffnet werden können und dem Spieler einen zufälligen Gewinn basierend auf vordefinierten Wahrscheinlichkeiten gewähren. Das System orientiert sich an bekannten Gaming-Mechaniken und bietet Spielern die Möglichkeit, durch Glück höhere Gewinne zu erzielen als den bezahlten Einsatz. Jede Lootbox enthält verschiedene Belohnungen mit unterschiedlichen Gewinnwahrscheinlichkeiten, wodurch ein Spannungselement entsteht.
Das Lootbox-System der Casino-Plattform umfasst sowohl die Verwaltung verschiedener Lootbox-Typen als auch ein animiertes Öffnungserlebnis, das an populäre Spiele wie Counter-Strike erinnert. Spieler können zwischen verschiedenen Lootboxes wählen, die sich in Preis und Belohnungsstruktur unterscheiden.
\subsubsection{Wahrscheinlichkeitssystem und Belohnungsverteilung}
Das Herzstück des Lootbox-Systems ist der Wahrscheinlichkeitsalgorithmus, der faire und nachvollziehbare Belohnungsverteilungen gewährleistet. Jede Belohnung in einer Lootbox hat eine definierte Wahrscheinlichkeit zwischen 0,0 und 1,0 (0\% bis 100\%). Die Gewinnermittlung erfolgt durch einen kumulativen Wahrscheinlichkeitsansatz:
\begin{enumerate}
\item Generierung einer Zufallszahl zwischen 0 und 1
\item Iteration durch alle Belohnungen mit Akkumulation der Wahrscheinlichkeiten
\item Rückgabe der ersten Belohnung, bei der die Zufallszahl kleiner oder gleich der kumulativen Wahrscheinlichkeit ist
\item Fallback zur letzten Belohnung bei keiner Übereinstimmung (Sicherheitsmechanismus)
\end{enumerate}
Dieses System ermöglicht eine präzise Kontrolle über die Verteilung der Belohnungen und gewährleistet, dass seltene Gegenstände entsprechend ihrer konfigurierten Wahrscheinlichkeit auftreten.
\subsubsection{Seltenheitssystem}
Die Lootboxes verwenden ein sechsstufiges Seltenheitssystem, das visuell durch Farbkodierung dargestellt wird:
\begin{itemize}
\item \textbf{Common (Häufig):} 40\%+ Wahrscheinlichkeit - Blau
\item \textbf{Uncommon (Ungewöhnlich):} 20-40\% Wahrscheinlichkeit - Grün
\item \textbf{Rare (Selten):} 10-20\% Wahrscheinlichkeit - Blau
\item \textbf{Epic (Episch):} 5-10\% Wahrscheinlichkeit - Lila
\item \textbf{Legendary (Legendär):} 1-5\% Wahrscheinlichkeit - Gold
\item \textbf{Mythic (Mythisch):} <1\% Wahrscheinlichkeit - Rot
\end{itemize}
Die Farbkodierung wird sowohl in der Auswahlansicht als auch während der Öffnungsanimation verwendet, um Spielern sofort die Seltenheit ihrer Belohnung zu kommunizieren.
\subsubsection{Animiertes Öffnungserlebnis}
Das Frontend bietet ein hochgradig animiertes Öffnungserlebnis, das die Spannung und das Engagement der Spieler steigert. Die Öffnungssequenz umfasst:
\begin{enumerate}
\item \textbf{Auswahlphase:} Rasteranzeige verfügbarer Lootboxes mit Preis und möglichen Belohnungen
\item \textbf{Öffnungsanimation:} 10-sekündige horizontale Scrollanimation mit 120 Gegenständen
\item \textbf{Gewinnermittlung:} Hervorhebung des Gewinnergegenstands an Position 60
\item \textbf{Ergebnisdarstellung:} Einblendanimation für Gewinn und Gewinn-/Verlustberechnung
\end{enumerate}
Die Animation generiert eine Walze mit 120 Gegenständen, wobei der tatsächliche Gewinn immer an der mittleren Position (60) platziert wird. Dies erzeugt den Eindruck einer echten Zufallsauswahl, obwohl das Ergebnis bereits vorher feststeht.
\subsubsection{Datenmodell und Verwaltung}
Das Backend verwendet ein flexibles Datenmodell, das die Erstellung und Verwaltung verschiedener Lootbox-Typen ermöglicht:
\textbf{LootBoxEntity:} Repräsentiert eine Lootbox mit Namen, Preis und zugehörigen Belohnungen. Die Beziehung zu Belohnungen erfolgt über eine Viele-zu-Viele-Verknüpfung, wodurch Belohnungen zwischen verschiedenen Lootboxes geteilt werden können.
\textbf{RewardEntity:} Definiert einzelne Belohnungen mit Wert und Wahrscheinlichkeit. Die Wahrscheinlichkeit wird als Dezimalzahl mit einer Genauigkeit von 5 Stellen und 2 Nachkommastellen gespeichert.
Die Verwaltung erfolgt über REST-Endpunkte, die sowohl das Abrufen verfügbarer Lootboxes als auch das Kaufen und Öffnen ermöglichen. Administrative Funktionen erlauben das Erstellen neuer Lootboxes und das Löschen bestehender.
\subsubsection{Kaufprozess und Guthaben-Integration}
Der Lootbox-Kaufprozess ist nahtlos in das Guthaben-Management-System der Plattform integriert:
\begin{enumerate}
\item Validierung des Benutzerguthabens gegen den Lootbox-Preis
\item Sofortige lokale Guthabenabzug für responsives Benutzererlebnis
\item Backend-API-Aufruf zur Kaufabwicklung
\item Server-seitige Belohnungsermittlung mittels Wahrscheinlichkeitsalgorithmus
\item Guthaben-Update (Preis abziehen, Belohnung hinzufügen)
\item Frontend-Synchronisation und Animationsstart
\end{enumerate}
Das System gewährleistet durch transaktionale Datenbankoperationen, dass Guthaben-Updates atomisch erfolgen und keine inkonsistenten Zustände entstehen können.
\subsubsection{Sicherheit und Fairness}
Die Lootbox-Implementierung legt besonderen Wert auf Sicherheit und Fairness:
\textbf{Server-seitige Autorität:} Alle Belohnungsermittlungen erfolgen ausschließlich auf dem Server, wodurch Client-seitige Manipulationen ausgeschlossen werden.
\textbf{Eingabevalidierung:} Sowohl die Datenübertragungsobjekte als auch die Backend-Services validieren alle Eingaben auf Korrektheit und Bereichsgrenzen.
\textbf{Wahrscheinlichkeitsintegrität:} Das System überwacht die Wahrscheinlichkeitswerte und gewährleistet, dass sie im gültigen Bereich (0,0-1,0) liegen.
Die Implementierung bietet somit ein vollständiges, produktionsreifes Lootbox-System mit robusten Frontend-Animationen, ordnungsgemäßer Backend-Validierung und nahtloser Benutzerintegration.

View file

@ -1,40 +0,0 @@
% !TEX root = ../Projektdokumentation.tex
\section{Projektarchitektur}
\label{sec:Projektarchitektur}
\subsection{Überblick}
Das Casino Gaming Platform Projekt folgt einer klassischen Client-Server-Architektur mit einer klaren Trennung zwischen Frontend und Backend. Diese Architektur wurde gewählt, um eine saubere Separation of Concerns zu gewährleisten und die Wartbarkeit sowie Erweiterbarkeit des Systems zu fördern. Die Kommunikation zwischen den beiden Schichten erfolgt über REST-\acs{API}s, die \acs{JSON}-Daten austauschen.
\subsection{Technologie-Stack}
\subsubsection{Frontend-Technologien}
Für die Entwicklung der Benutzeroberfläche wurde Angular\footnote{Angular - \url{https://angular.io/}} als Framework gewählt. Angular bietet eine robuste Basis für Single Page Applications und ermöglicht eine komponentenbasierte Entwicklung. Als Package Manager kommt Bun\footnote{Bun - \url{https://bun.sh/}} zum Einsatz, welcher sowohl die Paketinstallation als auch das Bundling übernimmt. Für das Styling wird Tailwind CSS\footnote{Tailwind CSS - \url{https://tailwindcss.com/}} verwendet, welches eine konsistente und effiziente Gestaltung der Benutzeroberfläche ermöglicht.
Für die Qualitätssicherung werden \acs{E2E}-Tests mit Playwright\footnote{Playwright - \url{https://playwright.dev/}} durchgeführt. Diese Tests stellen sicher, dass die gesamte Anwendung aus Benutzersicht korrekt funktioniert.
\subsubsection{Backend-Technologien}
Das Backend basiert auf Spring Boot, einem Java-Framework, das eine schnelle Entwicklung von produktionsreifen Anwendungen ermöglicht. Spring Boot wurde gewählt, da es umfangreiche Funktionalitäten out-of-the-box bietet und sich durch eine starke Community-Unterstützung auszeichnet. Als Build-Tool kommt Gradle zum Einsatz, welches flexiblere Konfigurationsmöglichkeiten als Maven bietet.
Für die Datenpersistierung wird PostgreSQL\footnote{PostgreSQL - \url{https://www.postgresql.org/}} verwendet.
\subsection{Systemarchitektur}
\subsubsection{Frontend-Architektur}
Das Frontend wurde als Single Page Application (SPA) konzipiert, um eine flüssige Benutzererfahrung zu gewährleisten. Die Architektur folgt Angulars modularem Ansatz und gliedert sich in verschiedene Bereiche: Feature-Module organisieren die Funktionalitäten nach Geschäftsbereichen wie Spiele und Einzahlungen. Wiederverwendbare UI-Komponenten wurden in einem Shared-Bereich zusammengefasst, um Code-Duplikation zu vermeiden.
Services übernehmen die Kommunikation mit dem Backend und kapseln die Geschäftslogik. \acs{HTTP}-Interceptors behandeln globale Aspekte wie Fehlerbehandlung zentral.
\subsubsection{Backend-Architektur}
Das Backend implementiert eine klassische mehrschichtige Architektur, die eine klare Trennung der Verantwortlichkeiten gewährleistet. Die Controller-Schicht stellt die REST-\acs{API}-Endpunkte bereit und behandelt \acs{HTTP}-Anfragen. Die Service-Schicht enthält die Geschäftslogik und orchestriert verschiedene Use Cases.
Die Repository-Schicht abstrahiert den Datenzugriff und verwendet Spring Data JPA für die Kommunikation mit der Datenbank. Entity-Klassen repräsentieren die Domain-Modelle und bilden die Datenbankstrukturen ab. Eine detaillierte Darstellung der verschiedenen Architekturschichten mit konkreten Code-Beispielen findet sich im Anhang (siehe \ref{app:CodeSchichten}).
\subsection{Datenarchitektur}
Die Datenbank folgt einem relationalen Design mit klar definierten Entitätsbeziehungen. Das Schema gliedert sich in mehrere Hauptbereiche: Der User Management Bereich verwaltet Benutzerkonten und Benutzerprofile. Spielbezogene Daten wie Spielstände, Wetten und Ergebnisse werden in separaten Tabellen gespeichert, um die Integrität der Spiellogik zu gewährleisten.
Der Financial Bereich behandelt alle monetären Transaktionen, Guthaben und Einzahlungen mit entsprechenden Audit-Trails für Compliance-Zwecke. Das Loot System verwaltet Lootboxen, deren Belohnungen und die zugehörigen Wahrscheinlichkeitsverteilungen, wobei besonderer Wert auf Transparenz und Fairness gelegt wird.
\subsection{Deployment-Strategie}
Die Deployment-Strategie wurde so konzipiert, dass sie sowohl lokale Entwicklung als auch produktive Umgebungen unterstützt. Für die lokale Entwicklung wird Docker Compose\footnote{Docker Compose - \url{https://docs.docker.com/compose/}} eingesetzt, welches eine konsistente Entwicklungsumgebung über verschiedene Entwicklerrechner hinweg gewährleistet.
Frontend und Backend können unabhängig voneinander gebaut und deployed werden, was eine flexible Entwicklung und Wartung ermöglicht. Diese Entkopplung erlaubt es verschiedenen Teams, parallel zu arbeiten, ohne sich gegenseitig zu blockieren. Verschiedene Umgebungskonfigurationen durch Profile stellen sicher, dass entwicklungs- und produktionsspezifische Einstellungen sauber getrennt sind und automatisiert angewendet werden können.

View file

@ -1,56 +0,0 @@
\section{Slots}
\subsection{Was ist Slots?}
Das Spiel 'Slots' ist eine digitale Implementierung eines klassischen Spielautomaten. Spieler setzen einen Betrag und drehen die Walzen, um Kombinationen von Symbolen zu erhalten. Ziel ist es, drei gleiche Symbole auf der mittleren Gewinnlinie zu erzielen, um einen Gewinn basierend auf dem Multiplikator des Symbols zu erhalten. Das Spiel bietet eine einfache und intuitive Möglichkeit, Glücksspiel zu erleben.
\subsubsection{Zufallszahlengenerierung}
Zur Generierung der Walzenpositionen verwendet diese Implementierung die Standardklasse \texttt{java.util.Random}. Für jede der drei Walzen wird eine pseudo-zufällige Zahl zwischen 0 und der \texttt{REEL\_LENGTH} (32) generiert. Diese Zahlen bestimmen die Startposition jedes Walzenstreifens. Die Walzenstreifen selbst werden bei der Initialisierung des \texttt{SlotService} einmalig erstellt und dann zufällig gemischt, um eine faire Verteilung der Symbole zu gewährleisten.
\subsubsection{Symbole und Auszahlungen}
Das Slots-Spiel verwendet verschiedene Symbole, die jeweils einen spezifischen Auszahlungsmultiplikator und eine bestimmte Häufigkeit auf den Walzenstreifen haben. Die Symbole sind:
\begin{itemize}
\item \textbf{SEVEN:} Höchster Auszahlungsmultiplikator (\texttt{1000x}). Seltenstes Symbol auf den Walzen.
\item \textbf{BAR:} Hoher Auszahlungsmultiplikator (\texttt{85x}).
\item \textbf{BELL:} Mittlerer Auszahlungsmultiplikator (\texttt{40x}).
\item \textbf{CHERRY:} Niedriger Auszahlungsmultiplikator (\texttt{10x}). Häufiger auf den Walzen.
\item \textbf{BLANK:} Kein Auszahlungsmultiplikator (\texttt{0x}). Dient als Platzhalter und führt bei dreifachem Erscheinen zu keinem Gewinn.
\end{itemize}
Die genaue Verteilung der Symbole auf den Walzenstreifen (\texttt{REEL\_LENGTH = 32}) ist wie folgt:
\begin{itemize}
\item SEVEN: 1 Vorkommen
\item BAR: 4 Vorkommen
\item BELL: 7 Vorkommen
\item CHERRY: 10 Vorkommen
\item BLANK: 10 Vorkommen
\end{itemize}
\subsubsection{Spielablauf und Datenfluss}
Der zentrale \texttt{SlotController} steuert den Spielablauf und empfängt die Anfragen vom Frontend. Jede Anfrage zum Drehen der Walzen (\texttt{/slots/spin}) enthält die Eckdaten des gewünschten Spins:
\begin{itemize}
\item \textbf{Einsatz:} Der gesetzte Münzbetrag.
\end{itemize}
Zuerst prüft der Controller das Guthaben des Spielers. Bei unzureichenden Mitteln wird der Vorgang abgelehnt. Andernfalls übergibt er die weitere Ausführung an die Dienstklasse (\texttt{SlotService}).
Die Dienstklasse (\texttt{SlotService}) übernimmt die eigentliche Logik des Slots-Spiels:
\begin{enumerate}
\item Zieht den Einsatz vom Spielerkonto ab.
\item Generiert zufällige Startpositionen für jede der drei Walzen.
\item Ermittelt die Symbole auf der mittleren Gewinnlinie basierend auf den generierten Positionen.
\item Bestimmt den Status des Spins (\texttt{WIN}, \texttt{LOSE}, \texttt{BLANK}) basierend auf den ermittelten Symbolen:
\begin{itemize}
\item \texttt{WIN}: Alle drei Symbole auf der Gewinnlinie sind identisch und keines davon ist \texttt{BLANK}.
\item \texttt{BLANK}: Alle drei Symbole auf der Gewinnlinie sind identisch und es handelt sich um \texttt{BLANK}-Symbole. Dies führt zu keinem Gewinn.
\item \texttt{LOSE}: Die Symbole auf der Gewinnlinie sind nicht alle identisch.
\end{itemize}
\item Berechnet den Gewinnbetrag bei einem \texttt{WIN}-Status (Einsatz $\times$ Auszahlungsmultiplikator des Gewinnsymbols) und schreibt ihn dem Spielerkonto gut. Bei \texttt{LOSE} oder \texttt{BLANK} wird kein Betrag gutgeschrieben.
\item Erstellt eine 3x3-Matrix der sichtbaren Symbole auf den Walzen, zentriert um die Gewinnlinie.
\end{enumerate}
Das Ergebnis des Spiels wird an das Frontend zurückgesendet und enthält:
\begin{itemize}
\item \textbf{Status:} Der Status des Spins (\texttt{win}, \texttt{lose}, \texttt{blank}).
\item \textbf{Amount:} Der gewonnene Betrag (bei \texttt{WIN}) oder der Einsatz (bei \texttt{LOSE}/\texttt{BLANK}).
\item \textbf{Result Matrix:} Eine 3x3-Matrix der Symbole, die auf den Walzen sichtbar sind.
\end{itemize}

View file

@ -1,27 +0,0 @@
\section{Vorbereitung}
\label{sec:Vorbereitung}
\subsection{Ist-Analyse}
Im Rahmen der durchgeführten Marktanalyse konnten mehrere relevante Marktlücken identifiziert werden, die das geplante Produkt gezielt adressieren kann. Ein zentrales Defizit betrifft die technologische Basis vieler bestehender Casino-Plattformen. Zahlreiche Anbieter setzen noch auf veraltete Technologien, was sich nachteilig auf Performance, Wartbarkeit und Erweiterbarkeit auswirkt. Moderne Webtechnologien und Frameworks könnten in diesem Zusammenhang klare Vorteile schaffen, etwa durch schnellere Ladezeiten und verbesserte Skalierbarkeit.
Im Bereich der Benutzerfreundlichkeit bestehen Defizite. Viele Plattformen bieten eine unzureichende mobile Nutzererfahrung oder verlangen komplexe, zeitaufwändige Registrierungsprozesse oft ohne Social-Login oder einfache Authentifizierung. Ein Beispiel hierfür ist der Zwang zu langwierigen Formularen, die Nutzer gerade auf mobilen Geräten häufig frustrieren und zum Abbruch des Vorgangs führen.
Ein weiteres Problem liegt bei den Sicherheitsstandards. In zahlreichen Anwendungen sind moderne Authentifizierungsverfahren wie OAuth2 oder Zwei-Faktor-Authentifizierung noch nicht implementiert. Dies hat nicht nur zur Folge, dass das Vertrauen der Nutzer geschwächt wird, sondern öffnet auch potenzielle Sicherheitslücken.
Schließlich bestehen Defizite in der Integration zeitgemäßer Zahlungsprozesse. Oft sind Einzahlungen umständlich und erfordern manuelle Schritte, da moderne Payment-APIs wie Stripe, Apple Pay oder Google Pay nicht eingebunden sind. Ein reibungsloser, mobiltauglicher Zahlungsfluss könnte sich in diesem Zusammenhang als entscheidender Wettbewerbsvorteil erweisen.
\subsection{Soll-Analyse}
Für das geplante Online-Casino wurde eine umfassende Soll-Analyse durchgeführt, die die zentralen funktionalen, technischen und sozialen Anforderungen beschreibt.
Im Bereich des Spieleportfolios ist eine ausgewogene Mischung aus klassischen und unterhaltsamen Spielen vorgesehen. Zu den angebotenen Spielen zählen Blackjack mit erweiterten Spieloptionen wie Split und Double Down sowie verschiedene Slotmaschinen, deren RTP (Return to Player) flexibel konfigurierbar sein soll. Das Angebot wird durch einfache Zufallsspiele wie Dice, Coinflip sowie unterhaltungsorientierte Features wie Lootboxen, Plinko und Poker ergänzt, um unterschiedliche Spieltypen anzusprechen und die Nutzer langfristig zu binden.
Die Benutzerverwaltung basiert auf einem modernen, sicheren Authentifizierungssystem. Die Anmeldung erfolgt über eine Multi-Provider-OAuth2-Lösung, beispielsweise mit GitHub oder Google-Konten. Die Sitzungsverwaltung erfolgt über JWT-basierte Tokens, ergänzt durch zusätzliche Sicherheitsmechanismen wie E-Mail-Verifizierung und die Möglichkeit zum Passwort-Reset, um Datenschutz und Benutzerkomfort gleichermaßen zu gewährleisten.
Das Finanzsystem ist auf Einzahlungen in Echtzeit ausgerichtet. Zu diesem Zweck wird eine Stripe-Integration implementiert, die einen reibungslosen und sicheren Zahlungsprozess ermöglicht. Intern erfolgt die Verwaltung der Guthaben über eine virtuelle Währung, deren Genauigkeit mithilfe von BigDecimal präzise nachvollziehbar bleibt. Eine lückenlos nachvollziehbare Transaktionshistorie mit Status-Tracking gewährleistet maximale Transparenz und Nachvollziehbarkeit aller Zahlungsvorgänge.
Gemäß den technischen Anforderungen ist ein responsives Design mit Fokus auf Mobile-First-Nutzung erforderlich, um eine hohe Benutzerfreundlichkeit auf verschiedenen Plattformen zu gewährleisten. Echtzeitaktualisierungen der Inhalte sollen ohne erneuten Seitenaufbau erfolgen, um eine nahtlose und dynamische Spielerfahrung zu gewährleisten. Es ist essenziell, dass sämtliche API-Kommunikation über gesicherte Schnittstellen abgewickelt wird, um die Integrität der Daten und den Schutz sensibler Informationen sicherzustellen.
Schließlich spielen auch die sozialen Funktionen eine wichtige Rolle. Ranglisten, eine motivierende In-Game-Währung sowie ein Fokus auf Transparenz im Umgang mit Daten, Spielverläufen und Zahlungsflüssen sollen die Community stärken, die Nutzerbindung fördern und das Vertrauen in das Produkt nachhaltig unterstützen. Das Produkt wird als moderne Webanwendung direkt über den Browser bereitgestellt und ist somit ohne Download oder Installation für den Nutzer zugänglich. Um das Online-Casino nutzen zu können, sind einige technische Voraussetzungen erforderlich. Für die Anwendung wird ein aktueller Webbrowser wie Google Chrome, Mozilla Firefox, Safari oder Microsoft Edge vorausgesetzt. Zudem ist JavaScript aktiviert sein, da dieses für die Darstellung der Spielmechanik und der Benutzeroberfläche essenziell ist. Für eine reibungslose Kommunikation mit dem Server und ein flüssiges Spielerlebnis ist eine stabile Internetverbindung erforderlich. Zusätzlich ist die Erlaubnis von Cookies erforderlich, um Sitzungsdaten zu speichern und personalisierte Inhalte oder Nutzertracking zu ermöglichen.
Die genannten technischen Voraussetzungen bilden die Grundlage für einen stabilen und benutzerfreundlichen Zugriff auf die Anwendung. Abgesehen von den Basiskomponenten ergeben sich jedoch weiterführende Anforderungen an die Systemarchitektur, insbesondere im Hinblick auf die Sicherheit und die Fairness der Spielergebnisse.
Eine zentrale Herausforderung bei der technischen Umsetzung des Online-Casinos liegt in der sicheren und manipulationssicheren Spielauswertung. Um die Integrität des Spiels zu gewährleisten und Manipulationen durch Nutzer im Frontend zu verhindern, müssen sämtliche Spielmechaniken serverseitig ausgewertet werden. Dies bedeutet, dass Zufallsberechnungen, Spielergebnisse und Gewinnermittlungen ausschließlich auf dem Server erfolgen, bevor sie an das Frontend übermittelt werden. Diese Architektur gewährleistet, dass die Spielchancen stets fair und unveränderbar bleiben. Gleichzeitig erfordert sie eine besonders zuverlässige Server-Infrastruktur, da sie sowohl in Echtzeit als auch mit hoher Verfügbarkeit arbeiten muss. Diese Anforderung stellt eine bedeutsame technische Einschränkung dar, die bei der Systemarchitektur, der Performanceoptimierung sowie der Skalierung von Beginn an zu berücksichtigen ist.

View file

@ -1,67 +0,0 @@
\section{Wirtschaftliche Betrachtung}
\subsection{Marktuntersuchung}
Das Online-Casino fokussiert sich in erster Linie auf eine junge, technikaffine Zielgruppe im Alter von 18 bis 35 Jahren. Diese Nutzergruppe verfügt über Erfahrungen mit digitalen Anwendungen, zeigt eine hohe Bereitschaft für Internetaktivitäteten und ist für neue, spielerische Unterhaltungsformate insbesondere im Bereich des Web-Gamings aufgeschlossen. Sie sucht nach kurzweiligen, optisch ansprechenden Angeboten, die jederzeit und ohne große Einstiegshürde zugänglich sind.
Als sekundäre Zielgruppe werden Gelegenheitsspieler angesprochen, die besonderen Wert auf eine einfache, benutzerfreundliche Bedienung legen. Für diese Nutzer steht nicht die Technik, sondern das unkomplizierte Spielvergnügen im Vordergrund. Die Nutzerinnen und Nutzer erwarten eine klare Navigation, intuitive Spielmechaniken und möglichst wenig Hürden bei Registrierung, Einzahlung und Nutzung. Durch die Fokussierung auf zwei Zielgruppen soll eine breite Nutzerbasis aufgebaut werden, die sowohl regelmäßige als auch spontane Spieler umfasst.
Die Erwartungshaltung dieser Zielgruppe umfasst ein unterhaltsames, fesselndes und optisch ansprechendes Spielerlebnis, das eine authentische Casino-Atmosphäre vermittelt. Dabei legen die Nutzer großen Wert auf eine intuitive Benutzeroberfläche, schnelle Ladezeiten sowie abwechslungsreiche Spiele wie Slots, Blackjack oder CoinFlip. Gleichzeitig legen sie großen Wert auf einen reibungslosen und sicheren Zugang zum Spiel. Aus diesem Grund erwarten sie selbstverständlich moderne Mechanismen für eine zuverlässige und geschützte Anmeldung und Nutzung.
Die Erwartungen der Zielgruppe geben wichtige Hinweise darauf, welche Funktionen und Eigenschaften ein erfolgreiches Produkt aufweisen muss. Um die gestellten Erwartungen jedoch auch unter wirtschaftlichen Gesichtspunkten bedienen zu können, ist eine Analyse des Marktvolumens und -potenzials unerlässlich.
Der Markt für Social Casinos und Online-Casinos mit Auszahlungsverweigerung ist weltweit wachsend. Laut Branchenanalysen (z.B. Statista, Business Research Insights) liegt das Volumen des globalen Social-Casino-Markts bei mehreren Milliarden USD pro Jahr, mit jährlichen Wachstumsraten im hohen einstelligen Prozentbereich. In Zielmärkten wie den USA, Europa und Teilen Asiens ist das Potenzial besonders hoch, insbesondere bei Nutzern, die Glücksspiel aus rechtlichen oder moralischen Gründen meiden, aber den Spielspaß schätzen.
Das vorhandene Absatzpotential ist jedoch stark eingeschränkt auf eine klar umrissene Zielgruppe. Es richtet sich an Nutzer, die Glücksspiele primär als Entertainment betrachten und sich der Tatsache bewusst sind, dass keine Auszahlung möglich ist. In diesem Segment könnte eine emotionale Markenbindung ähnlich wie bei Gaming- oder Streaming-Abonnements aufgebaut werden. Nichtsdestotrotz ist das Marktvolumen im Vergleich zu klassischen Casino-Plattformen begrenzt, da der Reiz vieler Online-Casinos im potenziellen Gewinn liegt.
Die Wettbewerbssituation für das geplante Online-Casino ohne Auszahlungsoption ist als intensiv und dynamisch zu bezeichnen, da sich das Produkt in einem stark umkämpften digitalen Unterhaltungsmarkt bewegt. Das Wettbewerbsumfeld wird dabei von zwei Hauptarten von Konkurrenzprodukten geprägt. Einerseits klassische Online-Casinos mit Echtgeld-Auszahlung, die aufgrund der Aussicht auf reale Geldgewinne für viele Nutzer besonders attraktiv sind. Es sei darauf hingewiesen, dass renommierte Anbieter wie 888casino, LeoVegas, Stake oder Tipico Casino in der Regel staatlich lizenziert sind, ein umfangreiches Spielangebot offerieren und von hoher Markenbekanntheit sowie intensiven Marketingaktivitäten profitieren. Andererseits existieren Social Casinos ohne Auszahlungsfunktion, die in direkter Konkurrenz zum geplanten Produkt stehen. Plattformen wie Slotomania, House of Fun, Big Fish Casino oder DoubleDown Casino ermöglichen den Einsatz von Echtgeld, bieten jedoch keine reale Auszahlung. Stattdessen liegt der Fokus auf Unterhaltung, Gamification, täglichen Belohnungen, virtuellen Währungen und exklusiven VIP-Features. Diese Angebote sind in der Regel als Apps verfügbar, verfügen über eine große Nutzerbasis und arbeiten mit durchdachten Mechaniken zur langfristigen Nutzerbindung.
Die enorme Verbreitung und Beliebtheit dieser Social-Casino-Plattformen wirft die Frage auf, welche konkreten Stärken und Schwächen diese bestehenden Angebote aufweisen. Ein näherer Blick auf die Vor- und Nachteile der Konkurrenzprodukte liefert wichtige Erkenntnisse für die strategische Ausrichtung.
So ist liegt ein zentraler Vorteil bestehender Social-Casino-Produkte in der transparenten Erwartungshaltung der Nutzer. Da keine Auszahlungen von Echtgeld vorgesehen sind, besteht kein Missverständnis über mögliche Gewinne. Zudem unterliegen diese Angebote häufig geringeren regulatorischen Anforderungen, da sie rechtlich nicht als klassisches Glücksspiel eingestuft werden und somit oft keine spezielle Lizenz benötigen. Der gezielte Einsatz von Gamification-Elementen und sozialen Funktionen wie Ranglisten oder täglichen Herausforderungen führt auf vielen dieser Plattformen zu einer hohen Nutzerbindung.
Gleichzeitig sind bei diesen Modellen gewisse Nachteile zu berücksichtigen. Da reale Gewinne ausgeschlossen sind, ist die Zahlungsbereitschaft der Nutzer in der Regel geringer. Wird das Belohnungssystem als wenig attraktiv oder zu repetitiv wahrgenommen, sinkt die Verweildauer auf der Plattform schnell. Des Weiteren bedienen sich zahlreiche dieser Produkte sogenannter Pay-to-Win-Mechaniken, welche den zahlenden Nutzern Vorteile gegenüber anderen Teilnehmern verschaffen. Dieser Umstand resultiert bei der Spielerschaft häufig in Frustration und Unzufriedenheit.
Um sich in dieser Wettbewerbslandschaft zu behaupten, muss sich das neue Produkt durch klare Kommunikation, hohe Benutzerfreundlichkeit, attraktive Preisgestaltung sowie ein überzeugendes Unterhaltungs- und Belohnungssystem von der Masse abzuheben, um sich erfolgreich zu behaupten. Die Transparenz über den Verzicht auf Auszahlungen sowie eine hochwertige, faire Spielerfahrung sind von entscheidender Bedeutung, um Vertrauen aufzubauen und langfristige Nutzerbindung zu erreichen.
Obwohl das geplante Geschäftsmodell einige Vorteile und einen innovativen Ansatz bietet, sollten auch dessen potenzielle Risiken nicht außer Acht gelassen werden. Rechtlich besteht die Möglichkeit, dass das Angebot in bestimmten Ländern dennoch dem Glücksspielrecht unterliegt, auch wenn keine Auszahlungen von Echtgeld erfolgen. Dies kann zu regulatorischen Hürden oder Einschränkungen bei der Vermarktung führen. Es empfiehlt sich, auch auf reputativer Ebene Vorsicht walten zu lassen. Das Modell könnte von Teilen der Öffentlichkeit oder den Medien als irreführend oder potenziell ausbeuterisch wahrgenommen werden, insbesondere wenn hohe In-App-Ausgaben ohne reale Gegenleistung entstehen.
Ein weiteres Risiko betrifft die Marktakzeptanz. Da keine echten Gewinne erzielt werden können, besteht die Gefahr, dass potenzielle Nutzer dem Angebot mit geringem Interesse begegnen oder es schnell wieder verlassen. Die Kaufschwelle ist in diesem Zusammenhang von großer Bedeutung. Ohne die Aussicht auf Rückgewinn oder reale Belohnung wäre die Bereitschaft, für virtuelle Inhalte Geld auszugeben, deutlich geringer als bei klassischen Echtgeld-Casinos.
Aus den genannten Chancen und Risiken ergeben sich klare Anforderungen für das Projekt. Es ist von besonderer Wichtigkeit, dass die Spielbedingungen stets transparent dargestellt werden und die Nutzer darüber informiert werden, dass keine Auszahlungen von Echtgeld erfolgen. Um rechtliche Schwierigkeiten zu vermeiden, ist zudem eine umfassende rechtliche Prüfung in allen relevanten Zielmärkten erforderlich. Auf echte Gewinnchancen muss verzichtet werden, dennoch ist das Nutzererlebnis attraktiv zu gestalten. Dies kann beispielsweise durch die Implementierung von Gamification-Elementen, ansprechendem visuellen Feedback und sozialen Features, welche die Unterhaltung fördern, realisiert werden. Gleichzeitig ist ein ethisches Branding erforderlich, das das Produkt deutlich von traditionellen Glücksspielanbietern abgrenzt. Schließlich sollten innovative Monetarisierungsmodelle zum Einsatz kommen, wie Abonnements, Mikrotransaktionen oder auch Spendenmechanismen, um wirtschaftlich erfolgreich zu sein.
\subsection{Marketing Mix}
\subsubsection{Preispolitik}
Bei der Markteinführung ist eine Positionierung auf einem niedrigen bis mittleren Preisniveau vorgesehen. Das Ziel besteht darin, die Einstiegshürde möglichst gering zu halten und dadurch eine breite Zielgruppe zum Ausprobieren zu motivieren. Nutzer können bereits mit geringen Beträgen ab etwa 1 bis 5 Euro spielen. Zur Steigerung der Attraktivität tragen Willkommensboni, Freispiele oder die Verdopplung der ersten Einzahlung bei. Diese Maßnahmen positionieren das Produkt klar als unterhaltungsorientiertes, risikoarmes Angebot, das sich bewusst von klassischen Echtgeld-Casinos mit hoher finanzieller Schwelle abgrenzt.
Im Laufe des Produktlebenszyklus ist eine flexible Anpassung des Preismodells vorgesehen. In der Einführungsphase werden Niedrigpreise und Rabatte eingesetzt, um Nutzer zu gewinnen. In der Wachstumsphase besteht die Möglichkeit, größere Pakete einzuführen, wie beispielsweise Monatsabonnements oder Coin-Bundles. Während der Reifephase rücken Maßnahmen wie das Up-Selling in den Fokus, etwa durch den Verkauf von kosmetischen Items, Sonderfunktionen oder VIP-Zugängen. In der Sättigungsphase dienen zeitlich begrenzte Rabatte, Sonderaktionen und Events der Reaktivierung inaktiver Nutzer. Preisänderungen sollen stets mit neuen Inhalten, Features oder Events kombiniert werden, um die Zahlungsbereitschaft zu steigern und zu legitimieren.
Als zentrale Preisstrategie kommt eine Kombination aus Promotions- und Bonusstrategien zum Einsatz. Hierzu zählen insbesondere Treueprämien und VIP-Stufenprogramme sowie zeitlich befristete Sonderangebote, wie etwa Freispiele am Wochenende. Die vorliegende Strategie zielt darauf ab, kurzfristig eine große Anzahl an Nutzern zu gewinnen, diese emotional an das Produkt zu binden und sie zur wiederholten Einzahlung zu motivieren, obwohl eine Auszahlung in Echtgeld nicht möglich ist.
Darüber hinaus sind Preisdifferenzierungen ein zentraler Bestandteil der Monetarisierung. Es ist eine segmentbasierte Differenzierung geplant, bei der Gelegenheitsspieler kleinere Pakete erhalten, während Vielspieler Zugang zu größeren, exklusiveren Angeboten haben. Darüber hinaus wäre auch eine zeitliche Preisdifferenzierung, wie etwa Rabatte zu bestimmten Tageszeiten oder saisonalen Events, vorstellbar. Ergänzend werden psychologische Preisstrategien eingesetzt, wie etwa die Preisgestaltung mit Schwellenwerten (z. B. 4,99 € statt 5,00 €), um die Zahlungsbereitschaft der Kunden auf subtilere Weise zu fördern.
Die Preisgestaltung verfolgt insgesamt eine wachstumsorientierte Strategie, die Nutzer sowohl in der frühen als auch in der langfristigen Nutzung des Produkts optimal anspricht und gleichzeitig wirtschaftlich tragfähig bleibt.
\subsubsection{Kommunikationspolitik}
Das Produkt soll mittels einer strategisch ausgerichteten Werbestrategie, die verschiedene Kommunikationskanäle einbezieht, bekannt gemacht werden. Im Mittelpunkt stehen dabei Social-Media-Plattformen wie Meta (Facebook), TikTok und Instagram. Dort besteht die Möglichkeit, visuell ansprechende Kurzvideos zu veröffentlichen, die Spielspaß, schnelle Action und Belohnungssysteme in den Vordergrund stellen. Des Weiteren empfiehlt sich die Durchführung von Influencer-Marketing, um eine authentische Reichweite und ein Vertrauenverhältnis aufzubauen. Darüber hinaus erweisen sich Mobile Ads, also In-App-Werbung in anderen Spielen oder Anwendungen, als besonders effektiv, da sie spielaffine und potenziell zahlungsbereite Nutzer direkt im Nutzungskontext erreichen.
Ein weiterer wichtiger Kanal ist das Suchmaschinenmarketing, insbesondere über Google Ads. In diesem Kontext ist die Implementierung zielgerichteter Kampagnen mit spezifischen Keywords wie "kostenlos spielen", "Online Slots", "Casino ohne Auszahlung" oder "Spielspaß mit Echtgeld" ratsam. Dabei ist es essenziell, den Nutzer stets einen eindeutigen Hinweis zu präsentieren, dass die Erzielung von Echtgeldgewinnen nicht möglich ist. Darüber hinaus besteht die Möglichkeit, Streaming-Plattformen wie Twitch und YouTube zu nutzen, indem mit Streamern kooperiert wird, die unterhaltsames Gameplay zeigen und dabei nicht den Gewinn, sondern das Spielerlebnis betonen.
In allen Kanälen ist eine klare und transparente Kommunikation von essentieller Bedeutung, in welcher darauf hingewiesen wird, dass keine Auszahlungsfunktion zur Verfügung steht. Die Werbung muss das Produkt eindeutig als Entertainment-Angebot und nicht als Glücksspiel mit Gewinnerwartung positionieren, um sowohl die Nutzererwartung korrekt zu steuern als auch rechtlichen Risiken vorzubeugen.
Die Werbemaßnahmen sollen darauf abzielen, zentrale Produkteigenschaften und Mehrwerte zu kommunizieren, die das geplante Unterhaltungs-Casino von der Konkurrenz abheben. Im Mittelpunkt steht dabei die technische und visuelle Qualität des Angebots, wie schnelle Ladezeiten, eine intuitive Bedienoberfläche sowie hochwertige Grafiken und flüssige Animationen sollen ein modernes und ansprechendes Nutzererlebnis vermitteln. Gleichzeitig wird großer Wert auf Transparenz gelegt, insbesondere durch klare Hinweise auf den Unterhaltungscharakter des Spiels, etwa durch Formulierungen wie "Kein echtes Glücksspiel reines Entertainment".
Darüber hinaus ist es von entscheidender Bedeutung, die niedrigen Einstiegshürden deutlich zu kommunizieren. Durch die Bereitstellung von Willkommensangeboten, geringen Einstiegskosten und einer einfachen Einzahlung über moderne Zahlungsschnittstellen wird die Hemmschwelle für neue Nutzer gesenkt. Im Bereich der Sicherheit wird durch die Implementierung von OAuth2 auf aktuelle Standards gesetzt, was ebenfalls als vertrauensbildendes Element in der Werbung hervorgehoben werden kann. Die Intention der kommunizierten Informationen ist es, das Produkt als leicht zugänglich, sicher, unterhaltsam und fair zu positionieren.
\subsubsection{Distributionspolitik}
Das Produkt wird als moderne Webanwendung direkt über den Browser bereitgestellt und ist somit ohne Download oder Installation für den Nutzer zugänglich. Um das Online-Casino nutzen zu können, sind einige technische Voraussetzungen erforderlich. Für die Anwendung wird ein aktueller Webbrowser wie Google Chrome, Mozilla Firefox, Safari oder Microsoft Edge vorausgesetzt. Zudem ist JavaScript aktiviert sein, da dieses für die Darstellung der Spielmechanik und der Benutzeroberfläche essenziell ist. Für eine reibungslose Kommunikation mit dem Server und ein flüssiges Spielerlebnis ist eine stabile Internetverbindung erforderlich. Zusätzlich ist die Erlaubnis von Cookies erforderlich, um Sitzungsdaten zu speichern und personalisierte Inhalte oder Nutzertracking zu ermöglichen.
Um ein optimales Spielerlebnis zu gewährleisten, wird die Nutzung auf einem Desktop-Computer oder Tablet mit ausreichend großem Bildschirm empfohlen, da dies die Übersichtlichkeit und Bedienbarkeit deutlich verbessert. Die Nutzung ist insbesondere dann stabil und sicher, wenn das Betriebssystem aktualisiert ist. Als aktuelle Versionen kommen in diesem Fall Windows 10 oder höher, macOS ab Version 11 sowie die jeweils aktuellen Versionen von Android oder iOS in Betracht. Die Webanwendung zeichnet sich insgesamt durch ihre Flexibilität und Plattformunabhängigkeit aus und ist mit gängigen Geräten und Standards problemlos kompatibel.
\subsection{Wirtschaftlichkeitsbetrachtung}
Die Wirtschaftlichkeit des geplanten Produkts ist grundsätzlich als sehr hoch einzuschätzen, sofern zentrale Rahmenbedingungen erfüllt sind. Ein entscheidender Vorteil besteht darin, dass sämtliche Einzahlungen als Einnahmen verbucht werden können, da es keine Auszahlungsverpflichtungen gibt. Dies ist ein klarer Unterschied zu klassischen Online-Casinos. Gleichzeitig fallen geringe variable Kosten pro Nutzer an, da weder physische Produkte hergestellt noch verschickt werden müssen und auch keine echten Geldgewinne ausbezahlt werden. Das Geschäftsmodell ist darüber hinaus hochgradig skalierbar. Ist die Plattform erst einmal technisch stabil aufgebaut, kann sie mit nur geringem zusätzlichem Aufwand von tausenden oder sogar Millionen Nutzern gleichzeitig genutzt werden.
Die möglichen Einnahmequellen sind vielfältig. Neben den klassischen Echtgeld-Einzahlungen bieten sich zusätzliche Monetarisierungswege an, etwa durch In-App-Verkäufe, wie kosmetische Upgrades oder VIP-Status, durch Werbung innerhalb der Plattform oder über Affiliate-Kooperationen mit passenden Partnern. Diese Faktoren tragen dazu bei, dass das Modell aus wirtschaftlicher Sicht äußerst attraktiv erscheint.
Die Rentabilität ist jedoch von mehreren kritischen Erfolgsfaktoren abhängig. Zu den erforderlichen Maßnahmen zählen die rechtliche Absicherung in den Zielmärkten, eine ausreichend hohe Nutzerakzeptanz trotz fehlender Auszahlungsfunktion und insbesondere die Fähigkeit, eine langfristige Kundenbindung durch ein unterhaltsames, faires und transparentes Spielerlebnis sicherzustellen.
Dennoch sind bei derartigen Unternehmungen stets die bestehenden Risiken zu berücksichtigen. Auf rechtlicher Ebene besteht die Gefahr, dass das Angebot mit echtem Glücksspiel verwechselt wird. Sollte der Eindruck entstehen, dass Nutzer reale Gewinne erwarten dürfen, so könnte dies rechtlich als Täuschung gewertet werden. Dies könnte mit strafrechtlichen Konsequenzen verbunden sein. Zudem existieren international unterschiedliche Regulierungen im Glücksspielrecht, die auch sogenannte Social Casinos betreffen können, etwa durch Lizenzpflichten oder strenge Anforderungen an die AGB und die Nutzeraufklärung.
Auch auf Ebene der Markenreputation können Risiken entstehen. Sollten Nutzer erst im Nachhinein erkennen, dass keine Auszahlung möglich ist, könnten sie sich getäuscht fühlen. Dies könnte sich in Form von negativen Bewertungen, Beschwerden oder sogar Shitstorms auf Social Media und Plattformen wie Trustpilot äußern.
Zudem ist der Markt von hoher Wettbewerbsintensität geprägt, in dem sowohl etablierte Online-Casinos als auch renommierte Anbieter von Social Games bereits eine starke Marktposition innehaben. Die Monetarisierung eines Produkts kann nur dann erfolgreich sein, wenn es gelingt, die Nutzer trotz fehlender Gewinnaussicht emotional an das Produkt zu binden und sie zum freiwilligen Erwerb virtueller Inhalte zu motivieren. An dieser Stelle wird sich letztlich entscheiden, ob das vorhandene wirtschaftliche Potenzial auch realisiert werden kann.

View file

@ -1,98 +0,0 @@
[![Creative Commons Lizenzvertrag](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)](http://creativecommons.org/licenses/by-sa/4.0/)
LaTeX-Vorlage zur IHK-Projektdokumentation für Fachinformatiker Anwendungsentwicklung von [Stefan Macke](http://fiae.link/LaTeXVorlageFIAE) ist lizenziert unter einer [Creative Commons Namensnennung - Weitergabe unter gleichen Bedingungen 4.0 International Lizenz](http://creativecommons.org/licenses/by-sa/4.0/).
### _Lizenz_
DER GEGENSTAND DIESER LIZENZ (WIE UNTER "SCHUTZGEGENSTAND" DEFINIERT) WIRD UNTER DEN BEDINGUNGEN DIESER CREATIVE COMMONS PUBLIC LICENSE ("CCPL", "LIZENZ" ODER "LIZENZVERTRAG") ZUR VERFÜGUNG GESTELLT. DER SCHUTZGEGENSTAND IST DURCH DAS URHEBERRECHT UND/ODER ANDERE GESETZE GESCHÜTZT. JEDE FORM DER NUTZUNG DES SCHUTZGEGENSTANDES, DIE NICHT AUFGRUND DIESER LIZENZ ODER DURCH GESETZE GESTATTET IST, IST UNZULÄSSIG.
DURCH DIE AUSÜBUNG EINES DURCH DIESE LIZENZ GEWÄHRTEN RECHTS AN DEM SCHUTZGEGENSTAND ERKLÄREN SIE SICH MIT DEN LIZENZBEDINGUNGEN RECHTSVERBINDLICH EINVERSTANDEN. SOWEIT DIESE LIZENZ ALS LIZENZVERTRAG ANZUSEHEN IST, GEWÄHRT IHNEN DER LIZENZGEBER DIE IN DER LIZENZ GENANNTEN RECHTE UNENTGELTLICH UND IM AUSTAUSCH DAFÜR, DASS SIE DAS GEBUNDENSEIN AN DIE LIZENZBEDINGUNGEN AKZEPTIEREN.
**1\. Definitionen**
1. Der Begriff "**Abwandlung**" im Sinne dieser Lizenz bezeichnet das Ergebnis jeglicher Art von Veränderung des Schutzgegenstandes, solange die eigenpersönlichen Züge des Schutzgegenstandes darin nicht verblassen und daran eigene Schutzrechte entstehen. Das kann insbesondere eine Bearbeitung, Umgestaltung, Änderung, Anpassung, Übersetzung oder Heranziehung des Schutzgegenstandes zur Vertonung von Laufbildern sein. Nicht als Abwandlung des Schutzgegenstandes gelten seine Aufnahme in eine Sammlung oder ein Sammelwerk und die freie Benutzung des Schutzgegenstandes.
2. Der Begriff "**Sammelwerk**" im Sinne dieser Lizenz meint eine Zusammenstellung von literarischen, künstlerischen oder wissenschaftlichen Inhalten, sofern diese Zusammenstellung aufgrund von Auswahl und Anordnung der darin enthaltenen selbständigen Elemente eine geistige Schöpfung darstellt, unabhängig davon, ob die Elemente systematisch oder methodisch angelegt und dadurch einzeln zugänglich sind oder nicht.
3. "**Verbreiten**" im Sinne dieser Lizenz bedeutet, den Schutzgegenstand oder Abwandlungen im Original oder in Form von Vervielfältigungsstücken, mithin in körperlich fixierter Form der Öffentlichkeit anzubieten oder in Verkehr zu bringen.
4. Unter "**Lizenzelementen**" werden im Sinne dieser Lizenz die folgenden übergeordneten Lizenzcharakteristika verstanden, die vom Lizenzgeber ausgewählt wurden und in der Bezeichnung der Lizenz zum Ausdruck kommen: "Namensnennung", "Weitergabe unter gleichen Bedingungen".
5. Der "**Lizenzgeber**" im Sinne dieser Lizenz ist diejenige natürliche oder juristische Person oder Gruppe, die den Schutzgegenstand unter den Bedingungen dieser Lizenz anbietet und insoweit als Rechteinhaberin auftritt.
6. "**Rechteinhaber**" im Sinne dieser Lizenz ist der Urheber des Schutzgegenstandes oder jede andere natürliche oder juristische Person oder Gruppe von Personen, die am Schutzgegenstand ein Immaterialgüterrecht erlangt hat, welches die in Abschnitt 3 genannten Handlungen erfasst und bei dem eine Einräumung von Nutzungsrechten oder eine Weiterübertragung an Dritte möglich ist.
7. Der Begriff "**Schutzgegenstand**" bezeichnet in dieser Lizenz den literarischen, künstlerischen oder wissenschaftlichen Inhalt, der unter den Bedingungen dieser Lizenz angeboten wird. Das kann insbesondere eine persönliche geistige Schöpfung jeglicher Art, ein Werk der kleinen Münze, ein nachgelassenes Werk oder auch ein Lichtbild oder anderes Objekt eines verwandten Schutzrechts sein, unabhängig von der Art seiner Fixierung und unabhängig davon, auf welche Weise jeweils eine Wahrnehmung erfolgen kann, gleichviel ob in analoger oder digitaler Form. Soweit Datenbanken oder Zusammenstellungen von Daten einen immaterialgüterrechtlichen Schutz eigener Art genießen, unterfallen auch sie dem Begriff "Schutzgegenstand" im Sinne dieser Lizenz.
8. Mit "**Sie**" bzw. "**Ihnen**" ist die natürliche oder juristische Person gemeint, die in dieser Lizenz im Abschnitt 3 genannte Nutzungen des Schutzgegenstandes vornimmt und zuvor in Hinblick auf den Schutzgegenstand nicht gegen Bedingungen dieser Lizenz verstoßen oder aber die ausdrückliche Erlaubnis des Lizenzgebers erhalten hat, die durch diese Lizenz gewährten Nutzungsrechte trotz eines vorherigen Verstoßes auszuüben.
9. Unter "**Öffentlich Zeigen**" im Sinne dieser Lizenz sind Veröffentlichungen und Präsentationen des Schutzgegenstandes zu verstehen, die für eine Mehrzahl von Mitgliedern der Öffentlichkeit bestimmt sind und in unkörperlicher Form mittels öffentlicher Wiedergabe in Form von Vortrag, Aufführung, Vorführung, Darbietung, Sendung, Weitersendung, zeit- und ortsunabhängiger Zugänglichmachung oder in körperlicher Form mittels Ausstellung erfolgen, unabhängig von bestimmten Veranstaltungen und unabhängig von den zum Einsatz kommenden Techniken und Verfahren, einschließlich drahtgebundener oder drahtloser Mittel und Einstellen in das Internet.
10. "**Vervielfältigen**" im Sinne dieser Lizenz bedeutet, mittels beliebiger Verfahren Vervielfältigungsstücke des Schutzgegenstandes herzustellen, insbesondere durch Ton- oder Bildaufzeichnungen, und umfasst auch den Vorgang, erstmals körperliche Fixierungen des Schutzgegenstandes sowie Vervielfältigungsstücke dieser Fixierungen anzufertigen, sowie die Übertragung des Schutzgegenstandes auf einen Bild- oder Tonträger oder auf ein anderes elektronisches Medium, gleichviel ob in digitaler oder analoger Form.
11. "**Mit Creative Commons kompatible Lizenz**" bezeichnet eine Lizenz, die unter https://creativecommons.org/compatiblelicenses aufgelistet ist und die durch Creative Commons als grundsätzlich zur vorliegenden Lizenz äquivalent akzeptiert wurde, da zumindest folgende Voraussetzungen erfüllt sind:
Diese mit Creative Commons kompatible Lizenz
1. enthält Bestimmungen, welche die gleichen Ziele verfolgen, die gleiche Bedeutung haben und die gleichen Wirkungen erzeugen wie die Lizenzelemente der vorliegenden Lizenz; und
2. erlaubt ausdrücklich das Lizenzieren von ihr unterstellten Abwandlungen unter vorliegender Lizenz, unter einer anderen rechtsordnungsspezifisch angepassten Creative-Commons-Lizenz mit denselben Lizenzelementen, wie sie die vorliegende Lizenz aufweist, oder unter der entsprechenden Creative-Commons-Unported-Lizenz.
**2\. Schranken des Immaterialgüterrechts**
Diese Lizenz ist in keiner Weise darauf gerichtet, Befugnisse zur Nutzung des Schutzgegenstandes zu vermindern, zu beschränken oder zu vereiteln, die Ihnen aufgrund der Schranken des Urheberrechts oder anderer Rechtsnormen bereits ohne Weiteres zustehen oder sich aus dem Fehlen eines immaterialgüterrechtlichen Schutzes ergeben.
**3\. Einräumung von Nutzungsrechten**
Unter den Bedingungen dieser Lizenz räumt Ihnen der Lizenzgeber - unbeschadet unverzichtbarer Rechte und vorbehaltlich des Abschnitts 3.e) - das vergütungsfreie, räumlich und zeitlich (für die Dauer des Schutzrechts am Schutzgegenstand) unbeschränkte einfache Recht ein, den Schutzgegenstand auf die folgenden Arten und Weisen zu nutzen ("unentgeltlich eingeräumtes einfaches Nutzungsrecht für jedermann"):
1. Den Schutzgegenstand in beliebiger Form und Menge zu vervielfältigen, ihn in Sammelwerke zu integrieren und ihn als Teil solcher Sammelwerke zu vervielfältigen;
2. Abwandlungen des Schutzgegenstandes anzufertigen, einschließlich Übersetzungen unter Nutzung jedweder Medien, sofern deutlich erkennbar gemacht wird, dass es sich um Abwandlungen handelt;
3. den Schutzgegenstand, allein oder in Sammelwerke aufgenommen, öffentlich zu zeigen und zu verbreiten;
4. Abwandlungen des Schutzgegenstandes zu veröffentlichen, öffentlich zu zeigen und zu verbreiten.
5. Bezüglich Vergütung für die Nutzung des Schutzgegenstandes gilt Folgendes:
1. **Unverzichtbare gesetzliche Vergütungsansprüche**: Soweit unverzichtbare Vergütungsansprüche im Gegenzug für gesetzliche Lizenzen vorgesehen oder Pauschalabgabensysteme (zum Beispiel für Leermedien) vorhanden sind, behält sich der Lizenzgeber das ausschließliche Recht vor, die entsprechende Vergütung einzuziehen für jede Ausübung eines Rechts aus dieser Lizenz durch Sie.
2. **Vergütung bei Zwangslizenzen**: Sofern Zwangslizenzen außerhalb dieser Lizenz vorgesehen sind und zustande kommen, verzichtet der Lizenzgeber für alle Fälle einer lizenzgerechten Nutzung des Schutzgegenstandes durch Sie auf jegliche Vergütung.
3. **Vergütung in sonstigen Fällen**: Bezüglich lizenzgerechter Nutzung des Schutzgegenstandes durch Sie, die nicht unter die beiden vorherigen Abschnitte (i) und (ii) fällt, verzichtet der Lizenzgeber auf jegliche Vergütung, unabhängig davon, ob eine Einziehung der Vergütung durch ihn selbst oder nur durch eine Verwertungsgesellschaft möglich wäre.
Das vorgenannte Nutzungsrecht wird für alle bekannten sowie für alle noch nicht bekannten Nutzungsarten eingeräumt. Es beinhaltet auch das Recht, solche Änderungen am Schutzgegenstand vorzunehmen, die für bestimmte nach dieser Lizenz zulässige Nutzungen technisch erforderlich sind. Alle sonstigen Rechte, die über diesen Abschnitt hinaus nicht ausdrücklich durch den Lizenzgeber eingeräumt werden, bleiben diesem allein vorbehalten. Soweit Datenbanken oder Zusammenstellungen von Daten Schutzgegenstand dieser Lizenz oder Teil dessen sind und einen immaterialgüterrechtlichen Schutz eigener Art genießen, verzichtet der Lizenzgeber auf sämtliche aus diesem Schutz resultierenden Rechte.
**4\. Bedingungen**
Die Einräumung des Nutzungsrechts gemäß Abschnitt 3 dieser Lizenz erfolgt ausdrücklich nur unter den folgenden Bedingungen:
1. Sie dürfen den Schutzgegenstand ausschließlich unter den Bedingungen dieser Lizenz verbreiten oder öffentlich zeigen. Sie müssen dabei stets eine Kopie dieser Lizenz oder deren vollständige Internetadresse in Form des Uniform-Resource-Identifier (URI) beifügen. Sie dürfen keine Vertrags- oder Nutzungsbedingungen anbieten oder fordern, die die Bedingungen dieser Lizenz oder die durch diese Lizenz gewährten Rechte beschränken. Sie dürfen den Schutzgegenstand nicht unterlizenzieren. Bei jeder Kopie des Schutzgegenstandes, die Sie verbreiten oder öffentlich zeigen, müssen Sie alle Hinweise unverändert lassen, die auf diese Lizenz und den Haftungsausschluss hinweisen. Wenn Sie den Schutzgegenstand verbreiten oder öffentlich zeigen, dürfen Sie (in Bezug auf den Schutzgegenstand) keine technischen Maßnahmen ergreifen, die den Nutzer des Schutzgegenstandes in der Ausübung der ihm durch diese Lizenz gewährten Rechte behindern können. Dieser Abschnitt 4.a) gilt auch für den Fall, dass der Schutzgegenstand einen Bestandteil eines Sammelwerkes bildet, was jedoch nicht bedeutet, dass das Sammelwerk insgesamt dieser Lizenz unterstellt werden muss. Sofern Sie ein Sammelwerk erstellen, müssen Sie auf die Mitteilung eines Lizenzgebers hin aus dem Sammelwerk die in Abschnitt 4.c) aufgezählten Hinweise entfernen. Wenn Sie eine Abwandlung vornehmen, müssen Sie auf die Mitteilung eines Lizenzgebers hin von der Abwandlung die in Abschnitt 4.c) aufgezählten Hinweise entfernen.
2. Sie dürfen eine Abwandlung ausschließlich unter den Bedingungen
1. dieser Lizenz,
2. einer späteren Version dieser Lizenz mit denselben Lizenzelementen,
3. einer rechtsordnungsspezifischen Creative-Commons-Lizenz mit denselben Lizenzelementen ab Version 3.0 aufwärts (z.B. Namensnennung - Weitergabe unter gleichen Bedingungen 3.0 US),
4. der Creative-Commons-Unported-Lizenz mit denselben Lizenzelementen ab Version 3.0 aufwärts, oder
5. einer mit Creative Commons kompatiblen Lizenz
verbreiten oder öffentlich zeigen.
Falls Sie die Abwandlung gemäß Abschnitt (v) unter einer mit Creative Commons kompatiblen Lizenz lizenzieren, müssen Sie deren Lizenzbestimmungen Folge leisten.
Falls Sie die Abwandlungen unter einer der unter (i)-(iv) genannten Lizenzen ("Verwendbare Lizenzen") lizenzieren, müssen Sie deren Lizenzbestimmungen sowie folgenden Bestimmungen Folge leisten: Sie müssen stets eine Kopie der verwendbaren Lizenz oder deren vollständige Internetadresse in Form des Uniform-Resource-Identifier (URI) beifügen, wenn Sie die Abwandlung verbreiten oder öffentlich zeigen. Sie dürfen keine Vertrags- oder Nutzungsbedingungen anbieten oder fordern, die die Bedingungen der verwendbaren Lizenz oder die durch sie gewährten Rechte beschränken. Bei jeder Abwandlung, die Sie verbreiten oder öffentlich zeigen, müssen Sie alle Hinweise auf die verwendbare Lizenz und den Haftungsausschluss unverändert lassen. Wenn Sie die Abwandlung verbreiten oder öffentlich zeigen, dürfen Sie (in Bezug auf die Abwandlung) keine technischen Maßnahmen ergreifen, die den Nutzer der Abwandlung in der Ausübung der ihm durch die verwendbare Lizenz gewährten Rechte behindern können. Dieser Abschnitt 4.b) gilt auch für den Fall, dass die Abwandlung einen Bestandteil eines Sammelwerkes bildet, was jedoch nicht bedeutet, dass das Sammelwerk insgesamt der verwendbaren Lizenz unterstellt werden muss.
3. Die Verbreitung und das öffentliche Zeigen des Schutzgegenstandes oder auf ihm aufbauender Abwandlungen oder ihn enthaltender Sammelwerke ist Ihnen nur unter der Bedingung gestattet, dass Sie, vorbehaltlich etwaiger Mitteilungen im Sinne von Abschnitt 4.a), alle dazu gehörenden Rechtevermerke unberührt lassen. Sie sind verpflichtet, die Rechteinhaberschaft in einer der Nutzung entsprechenden, angemessenen Form anzuerkennen, indem Sie - soweit bekannt - Folgendes angeben:
1. Den Namen (oder das Pseudonym, falls ein solches verwendet wird) des Rechteinhabers und / oder, falls der Lizenzgeber im Rechtevermerk, in den Nutzungsbedingungen oder auf andere angemessene Weise eine Zuschreibung an Dritte vorgenommen hat (z.B. an eine Stiftung, ein Verlagshaus oder eine Zeitung) ("Zuschreibungsempfänger"), Namen bzw. Bezeichnung dieses oder dieser Dritten;
2. den Titel des Inhaltes;
3. in einer praktikablen Form den Uniform-Resource-Identifier (URI, z.B. Internetadresse), den der Lizenzgeber zum Schutzgegenstand angegeben hat, es sei denn, dieser URI verweist nicht auf den Rechtevermerk oder die Lizenzinformationen zum Schutzgegenstand;
4. und im Falle einer Abwandlung des Schutzgegenstandes in Übereinstimmung mit Abschnitt 3.b) einen Hinweis darauf, dass es sich um eine Abwandlung handelt.
Die nach diesem Abschnitt 4.c) erforderlichen Angaben können in jeder angemessenen Form gemacht werden; im Falle einer Abwandlung des Schutzgegenstandes oder eines Sammelwerkes müssen diese Angaben das Minimum darstellen und bei gemeinsamer Nennung mehrerer Rechteinhaber dergestalt erfolgen, dass sie zumindest ebenso hervorgehoben sind wie die Hinweise auf die übrigen Rechteinhaber. Die Angaben nach diesem Abschnitt dürfen Sie ausschließlich zur Angabe der Rechteinhaberschaft in der oben bezeichneten Weise verwenden. Durch die Ausübung Ihrer Rechte aus dieser Lizenz dürfen Sie ohne eine vorherige, separat und schriftlich vorliegende Zustimmung des Lizenzgebers und / oder des Zuschreibungsempfängers weder explizit noch implizit irgendeine Verbindung zum Lizenzgeber oder Zuschreibungsempfänger und ebenso wenig eine Unterstützung oder Billigung durch ihn andeuten.
4. Die oben unter 4.a) bis c) genannten Einschränkungen gelten nicht für solche Teile des Schutzgegenstandes, die allein deshalb unter den Schutzgegenstandsbegriff fallen, weil sie als Datenbanken oder Zusammenstellungen von Daten einen immaterialgüterrechtlichen Schutz eigener Art genießen.
5. Persönlichkeitsrechte bleiben - soweit sie bestehen - von dieser Lizenz unberührt.
**5\. Gewährleistung**
SOFERN KEINE ANDERS LAUTENDE, SCHRIFTLICHE VEREINBARUNG ZWISCHEN DEM LIZENZGEBER UND IHNEN GESCHLOSSEN WURDE UND SOWEIT MÄNGEL NICHT ARGLISTIG VERSCHWIEGEN WURDEN, BIETET DER LIZENZGEBER DEN SCHUTZGEGENSTAND UND DIE EINRÄUMUNG VON RECHTEN UNTER AUSSCHLUSS JEGLICHER GEWÄHRLEISTUNG AN UND ÜBERNIMMT WEDER AUSDRÜCKLICH NOCH KONKLUDENT GARANTIEN IRGENDEINER ART. DIES UMFASST INSBESONDERE DAS FREISEIN VON SACH- UND RECHTSMÄNGELN, UNABHÄNGIG VON DEREN ERKENNBARKEIT FÜR DEN LIZENZGEBER, DIE VERKEHRSFÄHIGKEIT DES SCHUTZGEGENSTANDES, SEINE VERWENDBARKEIT FÜR EINEN BESTIMMTEN ZWECK SOWIE DIE KORREKTHEIT VON BESCHREIBUNGEN. DIESE GEWÄHRLEISTUNGSBESCHRÄNKUNG GILT NICHT, SOWEIT MÄNGEL ZU SCHÄDEN DER IN ABSCHNITT 6 BEZEICHNETEN ART FÜHREN UND AUF SEITEN DES LIZENZGEBERS DAS JEWEILS GENANNTE VERSCHULDEN BZW. VERTRETENMÜSSEN EBENFALLS VORLIEGT.
**6\. Haftungsbeschränkung**
DER LIZENZGEBER HAFTET IHNEN GEGENÜBER IN BEZUG AUF SCHÄDEN AUS DER VERLETZUNG DES LEBENS, DES KÖRPERS ODER DER GESUNDHEIT NUR, SOFERN IHM WENIGSTENS FAHRLÄSSIGKEIT VORZUWERFEN IST, FÜR SONSTIGE SCHÄDEN NUR BEI GROBER FAHRLÄSSIGKEIT ODER VORSATZ, UND ÜBERNIMMT DARÜBER HINAUS KEINERLEI FREIWILLIGE HAFTUNG.
**7\. Erlöschen**
1. Diese Lizenz und die durch sie eingeräumten Nutzungsrechte erlöschen mit Wirkung für die Zukunft im Falle eines Verstoßes gegen die Lizenzbedingungen durch Sie, ohne dass es dazu der Kenntnis des Lizenzgebers vom Verstoß oder einer weiteren Handlung einer der Vertragsparteien bedarf. Mit natürlichen oder juristischen Personen, die Abwandlungen des Schutzgegenstandes oder diesen enthaltende Sammelwerke unter den Bedingungen dieser Lizenz von Ihnen erhalten haben, bestehen nachträglich entstandene Lizenzbeziehungen jedoch solange weiter, wie die genannten Personen sich ihrerseits an sämtliche Lizenzbedingungen halten. Darüber hinaus gelten die Ziffern 1, 2, 5, 6, 7, und 8 auch nach einem Erlöschen dieser Lizenz fort.
2. Vorbehaltlich der oben genannten Bedingungen gilt diese Lizenz unbefristet bis der rechtliche Schutz für den Schutzgegenstand ausläuft. Davon abgesehen behält der Lizenzgeber das Recht, den Schutzgegenstand unter anderen Lizenzbedingungen anzubieten oder die eigene Weitergabe des Schutzgegenstandes jederzeit einzustellen, solange die Ausübung dieses Rechts nicht einer Kündigung oder einem Widerruf dieser Lizenz (oder irgendeiner Weiterlizenzierung, die auf Grundlage dieser Lizenz bereits erfolgt ist bzw. zukünftig noch erfolgen muss) dient und diese Lizenz unter Berücksichtigung der oben zum Erlöschen genannten Bedingungen vollumfänglich wirksam bleibt.
**8\. Sonstige Bestimmungen**
1. Jedes Mal wenn Sie den Schutzgegenstand für sich genommen oder als Teil eines Sammelwerkes verbreiten oder öffentlich zeigen, bietet der Lizenzgeber dem Empfänger eine Lizenz zu den gleichen Bedingungen und im gleichen Umfang an, wie Ihnen in Form dieser Lizenz.
2. Jedes Mal wenn Sie eine Abwandlung des Schutzgegenstandes verbreiten oder öffentlich zeigen, bietet der Lizenzgeber dem Empfänger eine Lizenz am ursprünglichen Schutzgegenstand zu den gleichen Bedingungen und im gleichen Umfang an, wie Ihnen in Form dieser Lizenz.
3. Sollte eine Bestimmung dieser Lizenz unwirksam sein, so bleibt davon die Wirksamkeit der Lizenz im Übrigen unberührt.
4. Keine Bestimmung dieser Lizenz soll als abbedungen und kein Verstoß gegen sie als zulässig gelten, solange die von dem Verzicht oder von dem Verstoß betroffene Seite nicht schriftlich zugestimmt hat.
5. Diese Lizenz (zusammen mit in ihr ausdrücklich vorgesehenen Erlaubnissen, Mitteilungen und Zustimmungen, soweit diese tatsächlich vorliegen) stellt die vollständige Vereinbarung zwischen dem Lizenzgeber und Ihnen in Bezug auf den Schutzgegenstand dar. Es bestehen keine Abreden, Vereinbarungen oder Erklärungen in Bezug auf den Schutzgegenstand, die in dieser Lizenz nicht genannt sind. Rechtsgeschäftliche Änderungen des Verhältnisses zwischen dem Lizenzgeber und Ihnen sind nur über Modifikationen dieser Lizenz möglich. Der Lizenzgeber ist an etwaige zusätzliche, einseitig durch Sie übermittelte Bestimmungen nicht gebunden. Diese Lizenz kann nur durch schriftliche Vereinbarung zwischen Ihnen und dem Lizenzgeber modifiziert werden. Derlei Modifikationen wirken ausschließlich zwischen dem Lizenzgeber und Ihnen und wirken sich nicht auf die Dritten gemäß Ziffern 8.a) und b) angeboteten Lizenzen aus.
6. Sofern zwischen Ihnen und dem Lizenzgeber keine anderweitige Vereinbarung getroffen wurde und soweit Wahlfreiheit besteht, findet auf diesen Lizenzvertrag das Recht der Bundesrepublik Deutschland Anwendung.

View file

@ -1,247 +0,0 @@
import { NgClass, NgIf, CurrencyPipe, CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
inject,
OnInit,
signal,
ViewChild,
} from '@angular/core';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
import { catchError, finalize } from 'rxjs/operators';
import { of } from 'rxjs';
import { AuthService } from '@service/auth.service';
import { AudioService } from '@shared/services/audio.service';
import { CoinflipGame, CoinflipRequest } from './models/coinflip.model';
@Component({
selector: 'app-coinflip',
standalone: true,
imports: [AnimatedNumberComponent, CurrencyPipe, FormsModule, CommonModule, NgIf, NgClass],
templateUrl: './coinflip.component.html',
styleUrl: './coinflip.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class CoinflipComponent implements OnInit {
currentBet = signal(10);
balance = signal(0);
gameInProgress = signal(false);
isActionInProgress = signal(false);
gameResult = signal<CoinflipGame | null>(null);
betInputValue = signal(10);
errorMessage = signal('');
isInvalidBet = signal(false);
@ViewChild('coinElement') coinElement?: ElementRef;
audioService = inject(AudioService);
authService = inject(AuthService);
private http = inject(HttpClient);
private coinflipSound?: HTMLAudioElement;
ngOnInit(): void {
// Abonniere Benutzerupdates fuer Echtzeitaktualisierungen des Guthabens
this.authService.userSubject.subscribe((user) => {
if (user) {
this.balance.set(user.balance);
}
});
// Initialisiere Muenzwurf-Sound
this.coinflipSound = new Audio('/sounds/coinflip.mp3');
}
setBetAmount(percentage: number) {
const newBet = Math.floor(this.balance() * percentage);
this.betInputValue.set(newBet > 0 ? newBet : 1);
this.currentBet.set(this.betInputValue());
}
updateBet(event: Event) {
const inputElement = event.target as HTMLInputElement;
let value = Number(inputElement.value);
// Setze ungueltigen Einsatz-Status zurueck
this.isInvalidBet.set(false);
// Erzwinge Mindesteinsatz von 1
if (value <= 0) {
value = 1;
}
// Begrenze Einsatz auf verfuegbares Guthaben und zeige Feedback
if (value > this.balance()) {
value = this.balance();
// Visuelles Feedback anzeigen
this.isInvalidBet.set(true);
// Zeige den Fehler kurz an
setTimeout(() => this.isInvalidBet.set(false), 800);
// Aktualisiere das Eingabefeld direkt, um dem Benutzer den maximalen Wert anzuzeigen
inputElement.value = String(value);
}
// Aktualisiere Signale
this.betInputValue.set(value);
this.currentBet.set(value);
}
betHeads() {
this.placeBet('HEAD');
}
betTails() {
this.placeBet('TAILS');
}
private placeBet(side: 'HEAD' | 'TAILS') {
if (this.gameInProgress() || this.isActionInProgress()) return;
// Setze vorheriges Ergebnis zurueck
this.gameResult.set(null);
this.errorMessage.set('');
// Setze Spielstatus
this.gameInProgress.set(true);
this.isActionInProgress.set(true);
// Spiele Einsatz-Sound
this.audioService.playBetSound();
// Erstelle Einsatz-Anfrage
const request: CoinflipRequest = {
betAmount: this.currentBet(),
coinSide: side,
};
// API aufrufen
this.http
.post<CoinflipGame>('/backend/coinflip', request)
.pipe(
catchError((error) => {
console.error('Fehler beim Spielen von Coinflip:', error);
if (error.status === 400 && error.error.message.includes('insufficient')) {
this.errorMessage.set('Unzureichendes Guthaben');
} else {
this.errorMessage.set('Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
}
this.gameInProgress.set(false);
return of(null);
}),
finalize(() => {
this.isActionInProgress.set(false);
})
)
.subscribe((result) => {
if (!result) return;
console.log('API-Antwort:', result);
// Behebe moegliche Inkonsistenzen bei der Eigenschaftenbenennung vom Backend
const fixedResult: CoinflipGame = {
isWin: result.isWin ?? result.win,
payout: result.payout,
coinSide: result.coinSide,
};
console.log('Korrigiertes Ergebnis:', fixedResult);
// Spiele Muenzwurf-Animation und -Sound
this.playCoinFlipAnimation(fixedResult.coinSide);
// Setze Ergebnis nach Abschluss der Animation
setTimeout(() => {
this.gameResult.set(fixedResult);
// Aktualisiere Guthaben mit neuem Wert vom Auth-Service
this.authService.loadCurrentUser();
// Spiele Gewinn-Sound, wenn der Spieler gewonnen hat
if (fixedResult.isWin) {
this.audioService.playWinSound();
}
// Setze Spielstatus nach Anzeigen des Ergebnisses zurueck
setTimeout(() => {
this.gameInProgress.set(false);
}, 1500);
}, 1100); // Kurz nach Ende der Animation
});
}
private playCoinFlipAnimation(result: 'HEAD' | 'TAILS') {
if (!this.coinElement) return;
const coinEl = this.coinElement.nativeElement;
// Setze bestehende Animationen zurueck
coinEl.classList.remove('animate-to-heads', 'animate-to-tails');
// Setze alle Inline-Styles von vorherigen Animationen zurueck
coinEl.style.transform = '';
// Erzwinge Reflow, um Animation neu zu starten
void coinEl.offsetWidth;
// Spiele Muenzwurf-Sound
if (this.coinflipSound) {
this.coinflipSound.currentTime = 0;
this.coinflipSound
.play()
.catch((err) => console.error('Fehler beim Abspielen des Sounds:', err));
}
// Fuege passende Animationsklasse basierend auf dem Ergebnis hinzu
if (result === 'HEAD') {
coinEl.classList.add('animate-to-heads');
} else {
coinEl.classList.add('animate-to-tails');
}
console.log(`Animation angewendet fuer Ergebnis: ${result}`);
}
/**
* Validiert Eingabe waehrend der Benutzer tippt, um ungueltige Werte zu verhindern
*/
validateBetInput(event: KeyboardEvent) {
// Erlaube Navigationstasten (Pfeile, Entf, Ruecktaste, Tab)
const navigationKeys = ['ArrowLeft', 'ArrowRight', 'Delete', 'Backspace', 'Tab'];
if (navigationKeys.includes(event.key)) {
return;
}
// Erlaube nur Zahlen
if (!/^\d$/.test(event.key)) {
event.preventDefault();
return;
}
// Ermittle den Wert, der nach dem Tastendruck entstehen wuerde
const input = event.target as HTMLInputElement;
const currentValue = input.value;
const cursorPosition = input.selectionStart || 0;
const newValue =
currentValue.substring(0, cursorPosition) +
event.key +
currentValue.substring(input.selectionEnd || cursorPosition);
const numValue = Number(newValue);
// Verhindere Werte, die groesser als das Guthaben sind
if (numValue > this.balance()) {
event.preventDefault();
}
}
getResultClass() {
if (!this.gameResult()) return '';
const result = this.gameResult();
const isWinner = result?.isWin || result?.win;
return isWinner ? 'text-emerald-500' : 'text-accent-red';
}
}

View file

@ -1,38 +0,0 @@
package de.szut.casino.coinflip;
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
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.RestController;
import java.util.Optional;
@RestController
public class CoinflipController {
private final UserService userService;
private final BalanceService balanceService;
private final CoinflipService coinflipService;
public CoinflipController(UserService userService, BalanceService balanceService, CoinflipService coinflipService) {
this.userService = userService;
this.balanceService = balanceService;
this.coinflipService = coinflipService;
}
@PostMapping("/coinflip")
public ResponseEntity<Object> coinFlip(@RequestBody @Valid CoinflipDto coinflipDto) {
UserEntity user = userService.getCurrentUser();
if (!this.balanceService.hasFunds(user, coinflipDto)) {
throw new InsufficientFundsException();
}
return ResponseEntity.ok(coinflipService.play(user, coinflipDto));
}
}

View file

@ -1,35 +0,0 @@
package de.szut.casino.coinflip;
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.Random;
@Service
public class CoinflipService {
private final Random random;
private final BalanceService balanceService;
public CoinflipService(BalanceService balanceService, Random random) {
this.balanceService = balanceService;
this.random = random;
}
public CoinflipResult play(UserEntity user, CoinflipDto coinflipDto) {
this.balanceService.subtractFunds(user, coinflipDto.getBetAmount());
CoinSide coinSide = this.random.nextBoolean() ? CoinSide.HEAD : CoinSide.TAILS;
CoinflipResult coinflipResult = new CoinflipResult(false, BigDecimal.ZERO, coinSide);
if (coinSide == coinflipDto.getCoinSide()) {
coinflipResult.setWin(true);
BigDecimal payout = coinflipDto.getBetAmount().multiply(BigDecimal.TWO);
this.balanceService.addFunds(user, payout);
coinflipResult.setPayout(payout);
}
return coinflipResult;
}
}

View file

@ -1,92 +0,0 @@
package de.szut.casino.user;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Setter
@Getter
@Entity
@NoArgsConstructor
public class UserEntity {
@Id
@GeneratedValue
private Long id;
@Version
private Long version;
@Column(unique = true)
private String email;
@Column(unique = true)
private String username;
private String password;
@Column(precision = 19, scale = 2)
private BigDecimal balance;
private Boolean emailVerified = false;
private String verificationToken;
private String passwordResetToken;
@Enumerated(EnumType.STRING)
private AuthProvider provider = AuthProvider.LOCAL;
private String providerId;
public UserEntity(String email, String username, String password, BigDecimal balance, String verificationToken) {
this.email = email;
this.username = username;
this.password = password;
this.balance = balance;
this.verificationToken = verificationToken;
}
public UserEntity(String email, String username, AuthProvider provider, String providerId, BigDecimal balance) {
this.email = email;
this.username = username;
this.provider = provider;
this.providerId = providerId;
this.balance = balance;
this.emailVerified = true; // OAuth providers verify emails
}
public void addBalance(BigDecimal amountToAdd) {
if (amountToAdd == null || amountToAdd.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
if (this.balance == null) {
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);
}
public String getEmailAddress() {
return "${name} <${email}>".replace("${name}", this.username).replace("${email}", this.email);
}
}

View file

@ -1,53 +0,0 @@
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=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15}
app.frontend-host=${FE_URL:http://localhost:4200}
app.mail.authentication=${MAIL_AUTHENTICATION:false}
app.mail.host=${MAIL_HOST:localhost}
app.mail.port=${MAIL_PORT:1025}
app.mail.username=${MAIL_USER:null}
app.mail.password=${MAIL_PASS:null}
app.mail.from-address=${MAIL_FROM:casino@localhost}
app.mail.protocol=${MAIL_PROTOCOL:smtp}
spring.application.name=casino
# JWT Configuration
jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437}
jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000}
# Logging
logging.level.org.springframework.security=DEBUG
# Swagger
springdoc.swagger-ui.path=swagger
springdoc.swagger-ui.try-it-out-enabled=true
# GitHub OAuth2 Configuration
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID:Ov23lingzZsPn1wwACoK}
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET:4b327fb3b1ab67584a03bcb9d53fa6439fbccad7}
spring.security.oauth2.client.registration.github.redirect-uri=${app.frontend-host}/oauth2/callback/github
spring.security.oauth2.client.registration.github.scope=user:email,read:user
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
spring.security.oauth2.client.provider.github.user-name-attribute=login
# OAuth Success and Failure URLs
app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback
# Google OAuth2 Configuration
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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}
spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google
spring.security.oauth2.client.registration.google.scope=email,profile
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
spring.security.oauth2.client.provider.google.user-name-attribute=sub

View file

@ -1,14 +0,0 @@
% !TEX root = Projektdokumentation.tex
% Hinweis: der Titel muss zum Inhalt des Projekts passen und den zentralen Inhalt des Projekts deutlich herausstellen
\newcommand{\titel}{Trustworthy Casino}
\newcommand{\untertitel}{Online-Spiele Casino}
\newcommand{\kompletterTitel}{\titel{} -- \untertitel}
\newcommand{\autorName}{Phan Huy Tran}
\newcommand{\betriebLogo}{hitec-logo.png}
\newcommand{\ausbildungsberuf}{Fachinformatiker für Anwendungsentwicklung}
\newcommand{\betreff}{Dokumentation}
\newcommand{\projektZeitraum}{29.01.2025 - 12.06.2025}

View file

@ -1,63 +0,0 @@
\documentclass[
11pt,
ngerman,
toc=listof, % Abbildungsverzeichnis sowie Tabellenverzeichnis in das Inhaltsverzeichnis aufnehmen
toc=bibliography, % Literaturverzeichnis in das Inhaltsverzeichnis aufnehmen
footnotes=multiple, % Trennen von direkt aufeinander folgenden Fußnoten
parskip=half, % vertikalen Abstand zwischen Absätzen verwenden anstatt horizontale Einrückung von Folgeabsätzen
numbers=noendperiod % Den letzten Punkt nach einer Nummerierung entfernen (nach DIN 5008)
]{scrartcl}
\pdfminorversion=5 % erlaubt das Einfügen von pdf-Dateien bis Version 1.7, ohne eine Fehlermeldung zu werfen (keine Garantie für fehlerfreies Einbetten!)
\usepackage[utf8]{inputenc} % muss als erstes eingebunden werden, da Meta/Packages ggfs. Sonderzeichen enthalten
\input{Meta} % Metadaten zu diesem Dokument (Autor usw.)
\input{Allgemein/Packages} % verwendete Packages
\input{Allgemein/Seitenstil} % Definitionen zum Aussehen der Seiten
\input{Allgemein/Befehle} % eigene allgemeine Befehle, die z.B. die Arbeit mit LaTeX erleichtern
\input{Befehle} % eigene projektspezifische Befehle, z.B. Abkürzungen usw.
\begin{document}
\phantomsection
\thispagestyle{plain}
\pdfbookmark[1]{Deckblatt}{deckblatt}
\input{Deckblatt}
\cleardoublepage
% Preface --------------------------------------------------------------------
\phantomsection
\pagenumbering{Roman}
\pdfbookmark[1]{Inhaltsverzeichnis}{inhalt}
\tableofcontents % Compile multiple times (e.g., 2-3 times) to ensure the Table of Contents is generated correctly.
\cleardoublepage
\phantomsection
\listoffigures
\cleardoublepage
\phantomsection
\cleardoublepage
\phantomsection
\cleardoublepage
\newcommand{\abkvz}{Abkürzungsverzeichnis}
\renewcommand{\nomname}{\abkvz}
\section*{\abkvz}
\markboth{\abkvz}{\abkvz}
\addcontentsline{toc}{section}{\abkvz}
\input{Abkuerzungen}
\clearpage
% Inhalt ---------------------------------------------------------------------
\pagenumbering{arabic}
\input{Inhalt.tex}
% Anhang ---------------------------------------------------------------------
\clearpage
\appendix
\pagenumbering{Roman}
\input{Anhang}
\end{document}

View file

@ -1,12 +0,0 @@
# LaTeX-Vorlage zur Projektdokumentation für Fachinformatiker Anwendungsentwicklung
Die Vorlage umfasst neben dem Layout und den obligatorischen Elementen wie Deckblatt, Verzeichnissen und Literaturangaben insbesondere eine Vorstrukturierung der üblicherweise geforderten Inhalte einer Dokumentation zur betrieblichen Projektarbeit inklusive einiger Beispiele für wichtige Inhalte wie z.B. Kostenkalkulation und Amortisationsrechnung. Obwohl viele Inhalte der Vorlage speziell auf Anwendungsentwickler zugeschnitten sind, dürfte die Vorlage auch für die anderen IT-Berufe (Fachinformatiker Systemintegration, IT-Kaufleute usw.) interessant sein, da die Vorgaben hinsichtlich der Projektarbeit größtenteils übereinstimmen.
Mehr Informationen und eine Beispieldokumentation auf Basis dieser Vorlage gibt es hier: [Vorlage für die Projektdokumentation][fiaevorlage].
[fiaevorlage]: http://fiae.link/LaTeXVorlageFIAE "Vorlage für die Projektdokumentation"
# Lizenz
[![Creative Commons Lizenzvertrag](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)](http://creativecommons.org/licenses/by-sa/4.0/)
LaTeX-Vorlage zur IHK-Projektdokumentation für Fachinformatiker Anwendungsentwicklung von [Stefan Macke](http://fiae.link/LaTeXVorlageFIAE) ist lizenziert unter einer [Creative Commons Namensnennung - Weitergabe unter gleichen Bedingungen 4.0 International Lizenz](http://creativecommons.org/licenses/by-sa/4.0/).

View file

@ -1,11 +0,0 @@
% Table generated by Excel2LaTeX from sheet 'Kostenaufstellung'
\begin{tabular}{lrlr}
\rowcolor{heading}\textbf{Vorgang} & \textbf{Zeit} & \textbf{Kosten pro Stunde} & \textbf{Kosten} \\
Entwicklungskosten & 70 \mbox{h} & $\eur{10} + \eur{15} = \eur{25}$ & \eur{1750} \\
\rowcolor{odd}Fachgespräch & 3 \mbox{h} & $\eur{25} + \eur{15} = \eur{40}$ & \eur{120} \\
Abnahmetest & 1 \mbox{h} & $\eur{25} + \eur{15} = \eur{40}$ & \eur{40} \\
\rowcolor{odd}Anwenderschulung & 25 \mbox{h} & $\eur{25} + \eur{15} = \eur{40}$ & \eur{1000} \\
\hline
\hline
\rowcolor{heading}\textbf{} & \textbf{} & \textbf{} & \textbf{\eur{2910}} \\
\end{tabular}

View file

@ -1,11 +0,0 @@
% Table generated by Excel2LaTeX from sheet 'Nutzwert'
\begin{tabular}{llllll}
\rowcolor{heading}\textbf{Eigenschaft} & \textbf{Gewichtung} & \textbf{Akelos} & \textbf{CakePHP} & \textbf{Symfony} & \textbf{Eigenentwicklung} \\
\textbf{Dokumentation} & 5 & 4 & 3 & 5 & 0 \\
\rowcolor{odd}\textbf{Reenginierung} & 3 & 4 & 2 & 5 & 3 \\
\textbf{Generierung} & 3 & 5 & 5 & 5 & 2 \\
\rowcolor{odd}\textbf{Testfälle} & 2 & 3 & 2 & 3 & 3 \\
\textbf{Standardaufgaben} & 4 & 3 & 3 & 3 & 0 \\
\rowcolor{heading}\textbf{Gesamt:} & \textbf{17} & \textbf{65} & \textbf{52} & \textbf{73} & \textbf{21} \\
\rowcolor{odd}\textbf{Nutzwert:} & & \textbf{3,82} & \textbf{3,06} & \textbf{4,29} & \textbf{1,24}\\
\end{tabular}

View file

@ -1,9 +0,0 @@
\begin{tabular}{lllll}
\rowcolor{heading}\textbf{Vorgang} & \textbf{Anzahl pro Monat} & \textbf{Zeit alt} & \textbf{Zeit neu} & \textbf{Einsparung} \\
Fehleranalyse & 10 & 30 min & 10 min & 200 min \\
\rowcolor{odd}Einarbeitung Modulstruktur & 2 & 120 min & 30 min & 180 min \\
Import von Daten & 5 & 10 min & 3 min & 35 min \\
\hline
\rowcolor{heading}\textbf{Zeiteinsparung Gesamt pro Monat} & & & & \textbf{415 min} \\
\end{tabular}

Some files were not shown because too many files have changed in this diff Show more