diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b1eaab8..97dbeeb 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -4,6 +4,38 @@ on: pull_request: jobs: + checkstyle: + name: "Checkstyle Main" + runs-on: "vps-4" + container: + image: "cimg/openjdk:22.0-node" + steps: + - name: "Checkout" + uses: actions/checkout@v3 + - name: Setup Java 22 + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "22" + - name: "Cache Gradle dependencies" + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-common + restore-keys: | + gradle-${{ runner.os }}- + - name: "Prepare Gradle" + working-directory: ./backend + run: gradle clean + - name: "Check" + working-directory: ./backend + run: gradle checkstyleMain + - name: "Stop Gradle" + working-directory: ./backend + run: gradle --stop + eslint: name: eslint runs-on: vps-4 @@ -14,6 +46,14 @@ jobs: uses: actions/checkout@v4 - name: Install bun uses: oven-sh/setup-bun@v2 + - uses: actions/cache@v3 + working-directory: ./frontend + with: + path: | + frontend/node_modules/ + key: ${{ runner.os }}-bun- + restore-keys: | + ${{ runner.os }}-bun- - name: Install dependencies run: | cd frontend @@ -33,6 +73,14 @@ jobs: uses: actions/checkout@v4 - name: Install bun uses: oven-sh/setup-bun@v2 + - uses: actions/cache@v3 + working-directory: ./frontend + with: + path: | + frontend/node_modules/ + key: ${{ runner.os }}-bun- + restore-keys: | + ${{ runner.os }}-bun- - name: Install dependencies run: | cd frontend @@ -52,6 +100,22 @@ jobs: uses: actions/checkout@v4 - name: Install bun uses: oven-sh/setup-bun@v2 + - uses: actions/cache@v3 + working-directory: ./frontend + with: + path: | + frontend/node_modules/ + key: ${{ runner.os }}-bun- + restore-keys: | + ${{ runner.os }}-bun- + - uses: actions/cache@v3 + working-directory: ./frontend + with: + path: | + frontend/dist/ + key: ${{ runner.os }}-dist- + restore-keys: | + ${{ runner.os }}-dist- - name: Install dependencies run: | cd frontend diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 565e524..76efd27 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -2,6 +2,21 @@ plugins { java id("org.springframework.boot") version "3.3.3" id("io.spring.dependency-management") version "1.1.6" + id("checkstyle") +} + +checkstyle { + configFile = file("$rootDir/config/checkstyle/checkstyle.xml") +} + +tasks.withType { + reports { + // Disable HTML report + html.required.set(false) + + // Disable XML report + xml.required.set(false) + } } group = "de.szut" diff --git a/backend/config/checkstyle/checkstyle.xml b/backend/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..bdcefb8 --- /dev/null +++ b/backend/config/checkstyle/checkstyle.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java b/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java index f102caf..7af90b9 100644 --- a/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java +++ b/backend/src/main/java/de/szut/casino/config/OpenAPIConfiguration.java @@ -58,4 +58,4 @@ public class OpenAPIConfiguration { } -} \ No newline at end of file +} diff --git a/backend/src/main/java/de/szut/casino/security/KeycloakLogoutHandler.java b/backend/src/main/java/de/szut/casino/security/KeycloakLogoutHandler.java index 2403c82..5e08794 100644 --- a/backend/src/main/java/de/szut/casino/security/KeycloakLogoutHandler.java +++ b/backend/src/main/java/de/szut/casino/security/KeycloakLogoutHandler.java @@ -45,4 +45,4 @@ public class KeycloakLogoutHandler implements LogoutHandler { } } -} \ No newline at end of file +} diff --git a/backend/src/main/java/de/szut/casino/user/UserController.java b/backend/src/main/java/de/szut/casino/user/UserController.java new file mode 100644 index 0000000..4d232ac --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserController.java @@ -0,0 +1,62 @@ +package de.szut.casino.user; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import de.szut.casino.user.dto.CreateUserDto; +import de.szut.casino.user.dto.GetUserDto; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/user/{id}") + public ResponseEntity getUser(@PathVariable String id) { + if (id == null || !userService.exists(id)) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(userService.getUser(id)); + } + + @PostMapping("/user") + public ResponseEntity createUser(@RequestBody @Valid CreateUserDto userData) { + if (userService.exists(userData.getKeycloakId())) { + + return this.redirect("/user/" + userData.getKeycloakId()); + } + + return ResponseEntity.ok(userService.createUser(userData)); + } + + @GetMapping("/user") + public ResponseEntity getCurrentUser(@RequestHeader("Authorization") String token) { + GetUserDto userData = userService.getCurrentUser(token); + + if (userData == null) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(userData); + } + + private ResponseEntity redirect(String route) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Location", route); + + return new ResponseEntity<>(headers, HttpStatus.FOUND); + } +} diff --git a/backend/src/main/java/de/szut/casino/user/UserEntity.java b/backend/src/main/java/de/szut/casino/user/UserEntity.java new file mode 100644 index 0000000..f6c1a5b --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserEntity.java @@ -0,0 +1,30 @@ +package de.szut.casino.user; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Setter +@Getter +@Entity +@NoArgsConstructor +public class UserEntity { + @Id + @GeneratedValue + private Long id; + @Column(unique = true) + private String keycloakId; + private String username; + private float balance; + + public UserEntity(String keycloakId, String username, float balance) { + this.keycloakId = keycloakId; + this.username = username; + this.balance = balance; + } +} diff --git a/backend/src/main/java/de/szut/casino/user/UserMappingService.java b/backend/src/main/java/de/szut/casino/user/UserMappingService.java new file mode 100644 index 0000000..e0183ec --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserMappingService.java @@ -0,0 +1,17 @@ +package de.szut.casino.user; + +import de.szut.casino.user.dto.CreateUserDto; +import de.szut.casino.user.dto.GetUserDto; +import org.springframework.stereotype.Service; + +@Service +public class UserMappingService { + public GetUserDto mapToGetUserDto(UserEntity user) { + return new GetUserDto(user.getKeycloakId(), user.getUsername(), user.getBalance()); + } + + public UserEntity mapToUserEntity(CreateUserDto createUserDto) { + return new UserEntity(createUserDto.getKeycloakId(), createUserDto.getUsername(), 0); + } +} + diff --git a/backend/src/main/java/de/szut/casino/user/UserRepository.java b/backend/src/main/java/de/szut/casino/user/UserRepository.java new file mode 100644 index 0000000..aaa5752 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserRepository.java @@ -0,0 +1,15 @@ +package de.szut.casino.user; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public interface UserRepository extends JpaRepository { + @Query("SELECT u FROM UserEntity u WHERE u.keycloakId = ?1") + Optional findOneByKeycloakId(String keycloakId); + + boolean existsByKeycloakId(String keycloakId); +} diff --git a/backend/src/main/java/de/szut/casino/user/UserService.java b/backend/src/main/java/de/szut/casino/user/UserService.java new file mode 100644 index 0000000..724962e --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/UserService.java @@ -0,0 +1,64 @@ +package de.szut.casino.user; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import de.szut.casino.user.dto.CreateUserDto; +import de.szut.casino.user.dto.GetUserDto; +import de.szut.casino.user.dto.KeycloakUserDto; + +@Service +public class UserService { + @Autowired + private UserRepository userRepository; + + @Autowired + private RestTemplate http; + + @Autowired + private UserMappingService mappingService; + + public UserEntity createUser(CreateUserDto createUserDto) { + UserEntity user = mappingService.mapToUserEntity(createUserDto); + userRepository.save(user); + + return user; + } + + public GetUserDto getUser(String keycloakId) { + Optional user = this.userRepository.findOneByKeycloakId(keycloakId); + + return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); + } + + public GetUserDto getCurrentUser(String token) { + KeycloakUserDto userData = getKeycloakUserInfo(token); + + if (userData == null) { + return null; + } + Optional user = this.userRepository.findOneByKeycloakId(userData.getSub()); + + return user.map(userEntity -> mappingService.mapToGetUserDto(userEntity)).orElse(null); + + } + + private KeycloakUserDto getKeycloakUserInfo(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + ResponseEntity response = this.http.exchange("http://localhost:9090/realms/LF12/protocol/openid-connect/userinfo", HttpMethod.GET, new HttpEntity<>(headers), KeycloakUserDto.class); + + return response.getBody(); + } + + public boolean exists(String keycloakId) { + return userRepository.existsByKeycloakId(keycloakId); + } +} diff --git a/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java new file mode 100644 index 0000000..ff28427 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/dto/CreateUserDto.java @@ -0,0 +1,15 @@ +package de.szut.casino.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class CreateUserDto { + private String keycloakId; + private String username; +} diff --git a/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java new file mode 100644 index 0000000..fb690af --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/dto/GetUserDto.java @@ -0,0 +1,16 @@ +package de.szut.casino.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GetUserDto { + private String keycloakId; + private String username; + private float balance; +} diff --git a/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java b/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java new file mode 100644 index 0000000..4238e13 --- /dev/null +++ b/backend/src/main/java/de/szut/casino/user/dto/KeycloakUserDto.java @@ -0,0 +1,15 @@ +package de.szut.casino.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class KeycloakUserDto { + private String sub; + private String preferred_username; +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0b7e617..2442d38 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - volumes: keycloak_data: postgres_data_keycloak_db: diff --git a/frontend/bun.lock b/frontend/bun.lock index 2154b1a..91de068 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,13 +5,19 @@ "name": "lf10-starter2024", "dependencies": { "@angular/animations": "^18.2.0", + "@angular/cdk": "~18.2.14", "@angular/common": "^18.2.0", "@angular/compiler": "^18.2.0", "@angular/core": "^18.2.0", "@angular/forms": "^18.2.0", + "@angular/material": "~18.2.14", "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@fortawesome/angular-fontawesome": "^1.0.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", "@stripe/stripe-js": "^5.6.0", "@tailwindcss/postcss": "^4.0.3", "keycloak-angular": "^16.0.1", @@ -73,6 +79,8 @@ "@angular/build": ["@angular/build@18.2.14", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.1802.14", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-syntax-import-attributes": "7.24.7", "@inquirer/confirm": "3.1.22", "@vitejs/plugin-basic-ssl": "1.1.0", "browserslist": "^4.23.0", "critters": "0.0.24", "esbuild": "0.23.0", "fast-glob": "3.3.2", "https-proxy-agent": "7.0.5", "listr2": "8.2.4", "lmdb": "3.0.13", "magic-string": "0.30.11", "mrmime": "2.0.0", "parse5-html-rewriting-stream": "7.0.0", "picomatch": "4.0.2", "piscina": "4.6.1", "rollup": "4.22.4", "sass": "1.77.6", "semver": "7.6.3", "vite": "5.4.14", "watchpack": "2.4.1" }, "peerDependencies": { "@angular/compiler-cli": "^18.0.0", "@angular/localize": "^18.0.0", "@angular/platform-server": "^18.0.0", "@angular/service-worker": "^18.0.0", "less": "^4.2.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0", "typescript": ">=5.4 <5.6" }, "optionalPeers": ["@angular/localize", "@angular/platform-server", "@angular/service-worker", "less", "postcss", "tailwindcss"] }, "sha512-9g24Oe/ZLULacW3hEpRCjSZIJPJTzN5BeFbA27epSV5NsrQOoeUGsEpRs90Zmt6eReO0fW1BGshWRoZtpSedcw=="], + "@angular/cdk": ["@angular/cdk@18.2.14", "", { "dependencies": { "tslib": "^2.3.0" }, "optionalDependencies": { "parse5": "^7.1.2" }, "peerDependencies": { "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-vDyOh1lwjfVk9OqoroZAP8pf3xxKUvyl+TVR8nJxL4c5fOfUFkD7l94HaanqKSRwJcI2xiztuu92IVoHn8T33Q=="], + "@angular/cli": ["@angular/cli@18.2.14", "", { "dependencies": { "@angular-devkit/architect": "0.1802.14", "@angular-devkit/core": "18.2.14", "@angular-devkit/schematics": "18.2.14", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", "@schematics/angular": "18.2.14", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", "listr2": "8.2.4", "npm-package-arg": "11.0.3", "npm-pick-manifest": "9.1.0", "pacote": "18.0.6", "resolve": "1.22.8", "semver": "7.6.3", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, "bin": { "ng": "bin/ng.js" } }, "sha512-kWgRRQtJPkr8iwN7DMbTi3sXOnv7H5QhbU/GgD3nNX3D8YCSPmnby4PAE/P3wn7FsIK9JsSchsCt7MZ37Urh9A=="], "@angular/common": ["@angular/common@18.2.13", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-4ZqrNp1PoZo7VNvW+sbSc2CB2axP1sCH2wXl8B0wdjsj8JY1hF1OhuugwhpAHtGxqewed2kCXayE+ZJqSTV4jw=="], @@ -85,6 +93,8 @@ "@angular/forms": ["@angular/forms@18.2.13", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "18.2.13", "@angular/core": "18.2.13", "@angular/platform-browser": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-A67D867fu3DSBhdLWWZl/F5pr7v2+dRM2u3U7ZJ0ewh4a+sv+0yqWdJW+a8xIoiHxS+btGEJL2qAKJiH+MCFfg=="], + "@angular/material": ["@angular/material@18.2.14", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", "@angular/cdk": "18.2.14", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", "@angular/platform-browser": "^18.0.0 || ^19.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "sha512-28pxzJP49Mymt664WnCtPkKeg7kXUsQKTKGf/Kl95rNTEdTJLbnlcc8wV0rT0yQNR7kXgpfBnG7h0ETLv/iu5Q=="], + "@angular/platform-browser": ["@angular/platform-browser@18.2.13", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "18.2.13", "@angular/common": "18.2.13", "@angular/core": "18.2.13" }, "optionalPeers": ["@angular/animations"] }, "sha512-tu7ZzY6qD3ATdWFzcTcsAKe7M6cJeWbT/4/bF9unyGO3XBPcNYDKoiz10+7ap2PUd0fmPwvuvTvSNJiFEBnB8Q=="], "@angular/platform-browser-dynamic": ["@angular/platform-browser-dynamic@18.2.13", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "18.2.13", "@angular/compiler": "18.2.13", "@angular/core": "18.2.13", "@angular/platform-browser": "18.2.13" } }, "sha512-kbQCf9+8EpuJC7buBxhSiwBtXvjAwAKh6MznD6zd2pyCYqfY6gfRCZQRtK59IfgVtKmEONWI9grEyNIRoTmqJg=="], @@ -367,6 +377,16 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], + "@fortawesome/angular-fontawesome": ["@fortawesome/angular-fontawesome@1.0.0", "", { "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.1", "tslib": "^2.8.1" }, "peerDependencies": { "@angular/core": "^19.0.0" } }, "sha512-EC2fYuXIuw2ld1kzJi+zysWus6OeGGfLQtbh0hW9zyyq5aBo8ZJkcJKBsVQ8E6Mg7nHyTWaXn+sdcXTPDWz+UQ=="], + + "@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@6.7.2", "", {}, "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="], + + "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.7.2", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" } }, "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA=="], + + "@fortawesome/free-brands-svg-icons": ["@fortawesome/free-brands-svg-icons@6.7.2", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" } }, "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q=="], + + "@fortawesome/free-solid-svg-icons": ["@fortawesome/free-solid-svg-icons@6.7.2", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" } }, "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], diff --git a/frontend/package.json b/frontend/package.json index 6d79510..c79aae0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,13 +14,19 @@ "private": true, "dependencies": { "@angular/animations": "^18.2.0", + "@angular/cdk": "~18.2.14", "@angular/common": "^18.2.0", "@angular/compiler": "^18.2.0", "@angular/core": "^18.2.0", "@angular/forms": "^18.2.0", + "@angular/material": "~18.2.14", "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "@fortawesome/angular-fontawesome": "^1.0.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", "@stripe/stripe-js": "^5.6.0", "@tailwindcss/postcss": "^4.0.3", "keycloak-angular": "^16.0.1", @@ -47,4 +53,4 @@ "typescript": "~5.5.2", "typescript-eslint": "8.23.0" } -} \ No newline at end of file +} diff --git a/frontend/public/blackjack.webp b/frontend/public/blackjack.webp new file mode 100644 index 0000000..a791c14 Binary files /dev/null and b/frontend/public/blackjack.webp differ diff --git a/frontend/public/liars-dice.webp b/frontend/public/liars-dice.webp new file mode 100644 index 0000000..df1fd1c Binary files /dev/null and b/frontend/public/liars-dice.webp differ diff --git a/frontend/public/lootbox.webp b/frontend/public/lootbox.webp new file mode 100644 index 0000000..710deed Binary files /dev/null and b/frontend/public/lootbox.webp differ diff --git a/frontend/public/plinko.webp b/frontend/public/plinko.webp new file mode 100644 index 0000000..c11370b Binary files /dev/null and b/frontend/public/plinko.webp differ diff --git a/frontend/public/poker.webp b/frontend/public/poker.webp new file mode 100644 index 0000000..9c60024 Binary files /dev/null and b/frontend/public/poker.webp differ diff --git a/frontend/public/slots.webp b/frontend/public/slots.webp new file mode 100644 index 0000000..5cf639a Binary files /dev/null and b/frontend/public/slots.webp differ diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 0680b43..41260d2 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1 +1,6 @@ - +
+
+ +
+ +
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 7dea888..bbc5fb6 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -2,11 +2,12 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { KeycloakAngularModule } from 'keycloak-angular'; +import { FooterComponent } from './shared/components/footer/footer.component'; @Component({ selector: 'app-root', standalone: true, - imports: [CommonModule, RouterOutlet, KeycloakAngularModule], + imports: [CommonModule, RouterOutlet, KeycloakAngularModule, FooterComponent], providers: [], templateUrl: './app.component.html', styleUrl: './app.component.css', diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index e7761ba..217efd4 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -4,6 +4,7 @@ import { provideExperimentalZonelessChangeDetection, } from '@angular/core'; import { provideRouter } from '@angular/router'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { routes } from './app.routes'; import { @@ -12,6 +13,7 @@ import { KeycloakService, } from 'keycloak-angular'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; export const initializeKeycloak = (keycloak: KeycloakService) => async () => keycloak.init({ @@ -25,7 +27,7 @@ export const initializeKeycloak = (keycloak: KeycloakService) => async () => onLoad: 'check-sso', silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', checkLoginIframe: false, - redirectUri: 'http://localhost:4200', + redirectUri: window.location.origin + '/', }, }); @@ -37,6 +39,7 @@ export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), KeycloakAngularModule, + FontAwesomeModule, { provide: APP_INITIALIZER, useFactory: initializeApp, @@ -51,5 +54,6 @@ export const appConfig: ApplicationConfig = { useClass: KeycloakBearerInterceptor, multi: true, }, + provideAnimationsAsync(), ], }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index b58c796..6fbef95 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,8 +1,6 @@ import { Routes } from '@angular/router'; import { LandingComponent } from './feature/landing/landing.component'; -import { HomeComponent } from './feature/home/home.component'; import { authGuard } from './auth.guard'; -import { DepositComponent } from './deposit/deposit.component'; export const routes: Routes = [ { @@ -10,13 +8,12 @@ export const routes: Routes = [ component: LandingComponent, }, { - path: 'home', - component: HomeComponent, - canActivate: [authGuard], + path: 'login/success', + loadComponent: () => import('./feature/login-success/login-success.component'), }, { - path: 'deposit', - component: DepositComponent, + path: 'home', + loadComponent: () => import('./feature/home/home.component'), canActivate: [authGuard], }, ]; diff --git a/frontend/src/app/auth.guard.ts b/frontend/src/app/auth.guard.ts index 0743ff6..035ccc8 100644 --- a/frontend/src/app/auth.guard.ts +++ b/frontend/src/app/auth.guard.ts @@ -1,23 +1,16 @@ -import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; +import { CanActivateFn, Router } from '@angular/router'; import { inject } from '@angular/core'; import { KeycloakService } from 'keycloak-angular'; -export const authGuard: CanActivateFn = async ( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot -) => { +export const authGuard: CanActivateFn = async () => { const keycloakService = inject(KeycloakService); - const isLoggedIn = keycloakService.isLoggedIn(); + const router = inject(Router); - if (isLoggedIn) { + if (keycloakService.isLoggedIn()) { return true; } - const baseurl = window.location.origin; - - keycloakService.login({ - redirectUri: `${baseurl}${state.url}`, - }); + router.navigate(['']); return false; }; diff --git a/frontend/src/app/deposit/deposit.component.html b/frontend/src/app/deposit/deposit.component.html deleted file mode 100644 index 1bdaf6e..0000000 --- a/frontend/src/app/deposit/deposit.component.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- {{ errorMsg }} -
-
- - -
- -
diff --git a/frontend/src/app/feature/deposit/deposit.component.html b/frontend/src/app/feature/deposit/deposit.component.html new file mode 100644 index 0000000..707d145 --- /dev/null +++ b/frontend/src/app/feature/deposit/deposit.component.html @@ -0,0 +1,21 @@ +

Guthaben aufladen

+ +
+
+ {{ errorMsg }} +
+
+ + +
+
+
+ + + + diff --git a/frontend/src/app/deposit/deposit.component.ts b/frontend/src/app/feature/deposit/deposit.component.ts similarity index 71% rename from frontend/src/app/deposit/deposit.component.ts rename to frontend/src/app/feature/deposit/deposit.component.ts index 0d56c70..36ec63f 100644 --- a/frontend/src/app/deposit/deposit.component.ts +++ b/frontend/src/app/feature/deposit/deposit.component.ts @@ -1,17 +1,30 @@ import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { loadStripe, Stripe } from '@stripe/stripe-js'; -import { DepositService } from '../service/deposit.service'; +import { DepositService } from '../../service/deposit.service'; import { debounceTime } from 'rxjs'; -import { environment } from '../../environments/environment'; +import { environment } from '../../../environments/environment'; import { NgIf } from '@angular/common'; +import { + MatDialogActions, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { MatButton } from '@angular/material/button'; @Component({ selector: 'app-deposit', standalone: true, - imports: [ReactiveFormsModule, NgIf], + imports: [ + ReactiveFormsModule, + NgIf, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatButton, + ], templateUrl: './deposit.component.html', - styleUrl: './deposit.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) export class DepositComponent implements OnInit { @@ -19,6 +32,7 @@ export class DepositComponent implements OnInit { protected errorMsg = ''; private stripe: Stripe | null = null; private service: DepositService = inject(DepositService); + public dialogRef: MatDialogRef = inject(MatDialogRef); async ngOnInit() { this.form = new FormGroup({ @@ -48,4 +62,8 @@ export class DepositComponent implements OnInit { this.stripe?.redirectToCheckout({ sessionId }); }); } + + public closeDialog(): void { + this.dialogRef.close(); + } } diff --git a/frontend/src/app/feature/home/home.component.html b/frontend/src/app/feature/home/home.component.html index 6a710c5..56a49cf 100644 --- a/frontend/src/app/feature/home/home.component.html +++ b/frontend/src/app/feature/home/home.component.html @@ -1,29 +1,91 @@ - -
-
-

Spiel Vorschau

-

Spiel Name

- -
-
-

Spiel Vorschau

-

Spiel Name

- -
-
-

Spiel Vorschau

-

Spiel Name

- +
+
+
+

Beliebte Spiele

+
+ + +
+
+ +
+
+
+
+ +
+
+

{{ game.name }}

+ +
+
+
+
+
+
+ +
+

Alle Spiele

+
+
+
+ +
+
+

{{ game.name }}

+ +
+
+
+
+
+
+
+ +
+
+

Konto

+
+ + +
+
+ +
+

Letzte Transaktionen

+
+
+
+

{{ transaction.type }}

+

{{ transaction.date }}

+
+ + {{ transaction.amount | currency: 'EUR' }} + +
+
+
+
diff --git a/frontend/src/app/feature/home/home.component.ts b/frontend/src/app/feature/home/home.component.ts index 06aa423..ebb2f02 100644 --- a/frontend/src/app/feature/home/home.component.ts +++ b/frontend/src/app/feature/home/home.component.ts @@ -1,19 +1,101 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { KeycloakService } from 'keycloak-angular'; +import { MatDialog } from '@angular/material/dialog'; +import { DepositComponent } from '../deposit/deposit.component'; +import { NavbarComponent } from '../../shared/components/navbar/navbar.component'; +import { CurrencyPipe, NgFor } from '@angular/common'; + +interface Game { + id: string; + name: string; + image: string; +} +interface Transaction { + id: string; + type: string; + amount: number; + date: string; +} @Component({ selector: 'app-homepage', standalone: true, - imports: [], + imports: [NavbarComponent, CurrencyPipe, NgFor], templateUrl: './home.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class HomeComponent { +export default class HomeComponent { private keycloakService: KeycloakService = inject(KeycloakService); + public dialog: MatDialog = inject(MatDialog); - logout() { + userAvatar = '/assets/images/default-avatar.png'; + username = this.keycloakService.getUsername(); + vipLevel = 1; + balance = 1000.0; + + featuredGames: Game[] = [ + { + id: '1', + name: 'Poker', + image: '/poker.webp', + }, + { + id: '2', + name: 'Blackjack', + image: '/blackjack.webp', + }, + { + id: '3', + name: 'Slots', + image: '/slots.webp', + }, + { + id: '4', + name: 'Plinko', + image: '/plinko.webp', + }, + { + id: '5', + name: 'Liars Dice', + image: '/liars-dice.webp', + }, + { + id: '6', + name: 'Lootboxen', + image: '/lootbox.webp', + }, + ]; + + allGames: Game[] = [...this.featuredGames]; + + recentTransactions: Transaction[] = [ + { + id: '1', + type: 'Deposit', + amount: 100.0, + date: '2024-03-20', + }, + { + id: '2', + type: 'Withdrawal', + amount: -50.0, + date: '2024-03-19', + }, + { + id: '3', + type: 'Bonus', + amount: 25.0, + date: '2024-03-18', + }, + ]; + + public logout() { const baseUrl = window.location.origin; this.keycloakService.logout(`${baseUrl}/`); } + + public openDialog() { + this.dialog.open(DepositComponent); + } } diff --git a/frontend/src/app/feature/landing/landing.component.html b/frontend/src/app/feature/landing/landing.component.html index 80d5974..597c71a 100644 --- a/frontend/src/app/feature/landing/landing.component.html +++ b/frontend/src/app/feature/landing/landing.component.html @@ -1,5 +1,142 @@ -@if (isLoggedIn) { - -} @else { - -} + + +
+
+
+
+

+ Willkommensbonus +

+
200% bis zu 500€
+

+ 200 Freispiele

+ + +
+ +
+

Beliebte Spiele

+
+
+
+
+
+
+

Slots

+

Klassische Spielautomaten

+ +
+
+
+
+

Plinko

+

Spannendes Geschicklichkeitsspiel

+ +
+
+ +
+ +
+
+
+

Poker

+

Texas Hold'em & mehr

+ +
+
+
+
+

Liars Dice

+

Würfelspiel mit Strategie

+ +
+
+ +
+
+
+ + + + +
+ +
+
+
+ +
+
+
50 Mio.€+
+
Ausgezahlt
+
+ +
+
10 Mio.+
+
Spiele
+
+ +
+
24/7
+
Support *
+
+
+
+
+
diff --git a/frontend/src/app/feature/landing/landing.component.ts b/frontend/src/app/feature/landing/landing.component.ts index d2e28c0..7fa92b6 100644 --- a/frontend/src/app/feature/landing/landing.component.ts +++ b/frontend/src/app/feature/landing/landing.component.ts @@ -1,21 +1,55 @@ -import { Component, inject } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; -import { RouterLink } from '@angular/router'; +import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core'; +import { NavbarComponent } from '../../shared/components/navbar/navbar.component'; +import { NgFor } from '@angular/common'; @Component({ - selector: 'app-landing', + selector: 'app-landing-page', standalone: true, - imports: [RouterLink], + imports: [NavbarComponent, NgFor], templateUrl: './landing.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LandingComponent { - private keycloakService: KeycloakService = inject(KeycloakService); +export class LandingComponent implements OnInit, OnDestroy { + currentSlide = 0; + private autoplayInterval: ReturnType | undefined; - public isLoggedIn = this.keycloakService.isLoggedIn(); + ngOnInit() { + this.startAutoplay(); + } - public login() { - const baseUrl = window.location.origin; + ngOnDestroy() { + this.stopAutoplay(); + } - this.keycloakService.login({ redirectUri: `${baseUrl}/home` }); + prevSlide() { + this.currentSlide = this.currentSlide === 0 ? 1 : 0; + this.resetAutoplay(); + } + + nextSlide() { + this.currentSlide = this.currentSlide === 1 ? 0 : 1; + this.resetAutoplay(); + } + + goToSlide(index: number) { + 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(); } } diff --git a/frontend/src/app/deposit/deposit.component.css b/frontend/src/app/feature/login-success/login-success.component.css similarity index 100% rename from frontend/src/app/deposit/deposit.component.css rename to frontend/src/app/feature/login-success/login-success.component.css diff --git a/frontend/src/app/feature/login-success/login-success.component.html b/frontend/src/app/feature/login-success/login-success.component.html new file mode 100644 index 0000000..ba9d449 --- /dev/null +++ b/frontend/src/app/feature/login-success/login-success.component.html @@ -0,0 +1 @@ +

Logging in...

diff --git a/frontend/src/app/feature/login-success/login-success.component.ts b/frontend/src/app/feature/login-success/login-success.component.ts new file mode 100644 index 0000000..bf81ac6 --- /dev/null +++ b/frontend/src/app/feature/login-success/login-success.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { UserService } from '../../service/user.service'; +import { KeycloakService } from 'keycloak-angular'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-login-success', + standalone: true, + imports: [], + templateUrl: './login-success.component.html', + styleUrl: './login-success.component.css', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export default class LoginSuccessComponent implements OnInit { + private userService: UserService = inject(UserService); + private keycloakService: KeycloakService = inject(KeycloakService); + private router: Router = inject(Router); + + async ngOnInit() { + const userProfile = await this.keycloakService.loadUserProfile(); + const user = await this.userService.getOrCreateUser(userProfile); + sessionStorage.setItem('user', JSON.stringify(user)); + + this.router.navigate(['']); + } +} diff --git a/frontend/src/app/model/User.ts b/frontend/src/app/model/User.ts new file mode 100644 index 0000000..a579b7a --- /dev/null +++ b/frontend/src/app/model/User.ts @@ -0,0 +1,5 @@ +export interface User { + keycloakId: string; + username: string; + balance: number; +} diff --git a/frontend/src/app/service/user.service.ts b/frontend/src/app/service/user.service.ts new file mode 100644 index 0000000..ba6bead --- /dev/null +++ b/frontend/src/app/service/user.service.ts @@ -0,0 +1,38 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { KeycloakProfile } from 'keycloak-js'; +import { catchError, EMPTY, Observable } from 'rxjs'; +import { User } from '../model/User'; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + private http: HttpClient = inject(HttpClient); + + public getUser(id: string): Observable { + return this.http.get(`/backend/user/${id}`).pipe(catchError(() => EMPTY)); + } + + public createUser(id: string, username: string): Observable { + return this.http.post('/backend/user', { + keycloakId: id, + username: username, + }); + } + + public async getOrCreateUser(userProfile: KeycloakProfile) { + if (userProfile.id == null) { + return; + } + return await this.getUser(userProfile.id) + .toPromise() + .then(async (user) => { + if (user) { + return user; + } + + return await this.createUser(userProfile.id ?? '', userProfile.username ?? '').toPromise(); + }); + } +} diff --git a/frontend/src/app/shared/components/footer/footer.component.html b/frontend/src/app/shared/components/footer/footer.component.html new file mode 100644 index 0000000..87c4682 --- /dev/null +++ b/frontend/src/app/shared/components/footer/footer.component.html @@ -0,0 +1,80 @@ +
+
+
+ + + +
+ +
+ + + + + + + +
+
+
+ +
+
+ +
+
+
+
diff --git a/frontend/src/app/shared/components/footer/footer.component.ts b/frontend/src/app/shared/components/footer/footer.component.ts new file mode 100644 index 0000000..1c3b309 --- /dev/null +++ b/frontend/src/app/shared/components/footer/footer.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faMoneyBillTransfer, faCreditCard, faWallet } from '@fortawesome/free-solid-svg-icons'; +import { faPaypal, faGooglePay, faApplePay } from '@fortawesome/free-brands-svg-icons'; + +@Component({ + selector: 'app-footer', + standalone: true, + templateUrl: './footer.component.html', + imports: [FontAwesomeModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FooterComponent { + currentYear: number = new Date().getFullYear(); + + faPaypal = faPaypal; + faCreditCard = faCreditCard; + faMoneyBillTransfer = faMoneyBillTransfer; + faWallet = faWallet; + faGooglePay = faGooglePay; + faApplePay = faApplePay; +} diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html new file mode 100644 index 0000000..fa492ae --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -0,0 +1,70 @@ + diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts new file mode 100644 index 0000000..53d1dee --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { KeycloakService } from 'keycloak-angular'; + +@Component({ + selector: 'app-navbar', + templateUrl: './navbar.component.html', + standalone: true, + imports: [RouterModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NavbarComponent { + isMenuOpen = false; + private keycloakService: KeycloakService = inject(KeycloakService); + + isLoggedIn = this.keycloakService.isLoggedIn(); + + login() { + try { + const baseUrl = window.location.origin; + this.keycloakService.login({ redirectUri: `${baseUrl}/login/success` }); + } catch (error) { + console.error('Login failed:', error); + } + } + + logout() { + this.keycloakService.logout(); + } + + toggleMenu() { + this.isMenuOpen = !this.isMenuOpen; + } +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 8de5d08..f78bf74 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,12 +1,147 @@ @import 'tailwindcss'; @theme { - --color-deep-blue: #0f212e; - --color-deep-blue-light: #1a2c38; - --color-deep-blue-contrast: #1b2c3b; - --color-light-blue: #1475e1; + --color-deep-blue: #0a1219; + --color-deep-blue-light: #121e27; + --color-deep-blue-contrast: #1a2835; + + --color-emerald: #10b981; + --color-emerald-dark: #059669; + --color-emerald-light: #34d399; + + --color-text-primary: #ffffff; + --color-text-secondary: #94a3b8; + --color-text-tertiary: #64748b; + + --color-accent-yellow: #fbbf24; + --color-accent-red: #ef4444; + --color-accent-purple: #8b5cf6; } body { - @apply bg-deep-blue text-gray-100; + @apply bg-deep-blue text-text-primary h-full; +} + +button, +a { + @apply cursor-pointer active:scale-95 text-text-primary transition-all duration-200; +} + +.card { + @apply bg-deep-blue-contrast rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300; +} + +.button-base { + @apply bg-emerald hover:bg-emerald-dark text-text-primary transition-all duration-300 active:scale-95 rounded; +} + +.game-card-content { + @apply p-4; +} + +.nav-button { + @apply hidden lg:block absolute top-1/2 -translate-y-1/2 bg-deep-blue-contrast hover:bg-deep-blue-contrast/90 text-text-primary p-3 rounded-full opacity-0 group-hover:opacity-100 transition-all duration-300 shadow-lg hover:scale-110; +} + +.slider-container { + @apply flex transition-transform duration-500 ease-out; +} + +.slider-grid { + @apply min-w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4; +} + +.welcome-bonus { + @apply text-4xl sm:text-5xl lg:text-7xl font-extrabold text-emerald-light mb-3 sm:mb-4; +} + +.bonus-description { + @apply text-text-secondary text-base sm:text-lg mb-6 sm:mb-8; +} + +.section-heading { + @apply font-bold text-text-primary; +} + +.game-heading-sm { + @apply font-bold text-text-primary text-sm mb-2; +} + +.game-heading-xl { + @apply font-bold text-text-primary text-xl mb-2; +} + +.game-text { + @apply text-text-secondary text-sm mb-4; +} + +.stat-container { + @apply bg-deep-blue-contrast rounded-lg shadow-lg p-4 sm:p-6 text-center; +} + +.stat-number { + @apply text-xl sm:text-2xl font-bold text-emerald; +} + +.stat-text { + @apply text-text-secondary text-sm; +} + +.nav-brand { + @apply flex items-center text-text-primary text-xl font-semibold; +} + +.nav-link { + @apply px-3 py-2 rounded-md font-normal text-sm text-text-secondary hover:text-text-primary hover:bg-deep-blue-contrast transition-all duration-200; +} + +.nav-toggle { + @apply text-text-secondary hover:text-text-primary transition-colors duration-200; +} + +.nav-mobile-menu { + @apply p-2 pt-2 mb-4 space-y-1 bg-deep-blue-contrast rounded-b-lg; +} + +.nav-mobile-link { + @apply block px-3 py-2 rounded-md text-sm text-text-secondary hover:text-text-primary hover:bg-deep-blue-light transition-all duration-200; +} + +.footer-section { + @apply col-span-2 md:col-span-1; +} + +.footer-heading { + @apply text-text-primary text-sm font-semibold mb-4; +} + +.footer-link { + @apply text-text-secondary hover:text-text-primary text-sm transition-all duration-200; +} + +.footer-payment-method { + @apply bg-deep-blue rounded p-3 flex items-center justify-center space-x-2 hover:bg-deep-blue/50 transition-all duration-200; +} + +.footer-payment-icon { + @apply text-text-secondary text-lg; +} + +.footer-payment-text { + @apply text-text-secondary text-xs whitespace-nowrap; +} + +.footer-copyright { + @apply text-text-secondary text-sm; +} + +.footer-disclaimer { + @apply text-xs; +} + +.mat-mdc-dialog-container { + --mdc-dialog-container-color: var(--color-deep-blue-light) important; + --mdc-dialog-subhead-color: var(--color-text-primary) important; + --mdc-dialog-supporting-text-color: var(--color-text-secondary) important; + --mdc-dialog-container-shape: 6px important; }