From 8eb27ba8b92d0aca7dc02b82020b069249e50665 Mon Sep 17 00:00:00 2001 From: shahdlala66 Date: Tue, 12 May 2026 22:18:09 +0200 Subject: [PATCH] fix: Merge branch 'main' of git.janis-eccarius.de:NowChess/NowChess-Frontend into feat/NCS-69 --- CHANGELOG.md | 20 ++ Dockerfile | 18 +- angular.json | 19 +- docker-entrypoint.sh | 18 + nginx.conf | 30 +- package.json | 3 +- public/env.template.js | 4 + src/app/app.routes.ts | 2 - src/app/app.ts | 7 +- src/app/core/config.loader.ts | 11 + src/app/pages/welcome/welcome.component.html | 59 ++++ src/app/pages/welcome/welcome.component.ts | 354 +++++++++++++++++++ src/environments/environment.staging.ts | 11 + src/environments/environment.ts | 12 +- src/index.html | 1 + versions.env | 3 + 16 files changed, 537 insertions(+), 35 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docker-entrypoint.sh create mode 100644 public/env.template.js create mode 100644 src/app/core/config.loader.ts create mode 100644 src/environments/environment.staging.ts create mode 100644 versions.env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..97ce0c0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +## 0.0.0 (2026-05-12) + +### Features + +* added bot, light and dark mode ([2de003e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2de003e497baee72f998d0d805ca1e58aababe48)) +* added web view 1v1 ([1828fa3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1828fa3275ddb8ce6bb90a9f6a970ec428ebce3a)) +* NCS-63 User account implementation ([#2](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/2)) ([ff75c8c](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/ff75c8ce2fad54137f04a14c15bc1d4a38fa9bb8)) +* NCS-75 Frontend Deployment Dockerfile ([#4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/4)) ([bd7ec58](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bd7ec581e38b5d8e775741bf16e19b4dc67b979e)) + +### Bug Fixes + +* build issues ([36d72fd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/36d72fd6cda41be51d28f8ac307dcdbcd31afa91)) +* cleaner components seperation ([8b090e4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8b090e4d96c64c0adb253e3aefad7930108ccfb9)) +* gitignore ([4da882f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/4da882f481ba7a008aac868fb37de7cb2bafea5d)) +* GITIGNORE ([8df2d05](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8df2d0550ab17c9afb2d19c414eac700a75add02)) +* npm ([c11c1d4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c11c1d4dce9de4bd5b463e891eebf961b37feb04)) +* removed .vs ([2833ead](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2833ead7be3b47ee5c188d2d21b7326cb3cb3f26)) +* removed cache files ([7ee59c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/7ee59c434bf137a08fd560bbc02ceefbcfd90f04)) +* size of pieces and removed text view of the game state ([c60d00f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c60d00f9d25247504845654615065fbccd7fe448)) +* structure ([3e8c7c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/3e8c7c4057e55aeec7cee8c24f6751ff24912c93)) diff --git a/Dockerfile b/Dockerfile index 1f05e67..0aafc7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,25 @@ -FROM node:20-alpine AS build +FROM --platform=$BUILDPLATFORM node:lts-alpine3.23 AS builder WORKDIR /app COPY package*.json ./ -RUN npm ci --silent +RUN npm install COPY . . +RUN npm run build -RUN npm run build -- --configuration production +FROM --platform=$TARGETPLATFORM nginx:stable-alpine AS production -FROM nginx:stable-alpine +RUN apk add --no-cache gettext + +RUN rm -rf /usr/share/nginx/html/* +COPY --from=builder /app/dist/nowchess-frontend/browser /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=build /app/dist/nowchess-frontend/browser /usr/share/nginx/html +COPY public/env.template.js /usr/share/nginx/html/env.template.js +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/angular.json b/angular.json index 3542eee..53f40a6 100644 --- a/angular.json +++ b/angular.json @@ -47,17 +47,25 @@ "budgets": [ { "type": "initial", - "maximumWarning": "500kB", - "maximumError": "4MB" + "maximumWarning": "1MB", + "maximumError": "5MB" }, { "type": "anyComponentStyle", - "maximumWarning": "1.5MB", - "maximumError": "2MB" + "maximumWarning": "1MB", + "maximumError": "5MB" } ], "outputHashing": "all" }, + "staging": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.staging.ts" + } + ] + }, "development": { "fileReplacements": [ { @@ -83,6 +91,9 @@ "production": { "buildTarget": "nowchess-frontend:build:production" }, + "staging": { + "buildTarget": "nowchess-frontend:build:staging" + }, "development": { "buildTarget": "nowchess-frontend:build:development" } diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..dab211f --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# Replace placeholders in env.template.js with environment variables and write env.js +TEMPLATE_PATH="/usr/share/nginx/html/env.template.js" +TARGET_PATH="/usr/share/nginx/html/env.js" + +if [ -f "$TEMPLATE_PATH" ]; then + echo "Rendering runtime config from $TEMPLATE_PATH" + echo "Using environment variables:" + printenv + echo "----" + envsubst < "$TEMPLATE_PATH" > "$TARGET_PATH" +else + echo "No runtime template found at $TEMPLATE_PATH, skipping" +fi + +exec nginx -g 'daemon off;' diff --git a/nginx.conf b/nginx.conf index 4a983a1..ff4e939 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,16 +1,22 @@ server { - listen 80; - server_name _; - root /usr/share/nginx/html; - index index.html; + listen 80; + server_name localhost; - location / { - try_files $uri $uri/ /index.html; - } + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } - location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ { - try_files $uri =404; - expires 1y; - add_header Cache-Control "public"; - } + location /env.js { + root /usr/share/nginx/html; + default_type application/javascript; + expires -1; + add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } } diff --git a/package.json b/package.json index 0882715..37a84ae 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng build", + "build": "ng build --configuration production", + "build:staging": "ng build --configuration staging", "watch": "ng build --watch --configuration development", "test": "ng test" }, diff --git a/public/env.template.js b/public/env.template.js new file mode 100644 index 0000000..72477ac --- /dev/null +++ b/public/env.template.js @@ -0,0 +1,4 @@ +window.__RUNTIME_CONFIG__ = { + API_URL: "${API_URL}", + WEBSOCKET_URL: "${WEBSOCKET_URL}" +}; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 59dd95e..9fb0ff3 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -2,12 +2,10 @@ import { Routes } from '@angular/router'; import { GameComponent } from './pages/game/game.component'; import { WelcomeComponent } from './pages/welcome/welcome.component'; import { ProfileComponent } from './pages/profile/profile.component'; -import { ChallengesComponent } from './pages/challenges/challenges.component'; export const routes: Routes = [ { path: '', component: WelcomeComponent }, { path: 'profile', component: ProfileComponent }, - { path: 'challenges', component: ChallengesComponent }, { path: 'game/:gameId', component: GameComponent }, { path: '**', redirectTo: '' } ]; diff --git a/src/app/app.ts b/src/app/app.ts index 5211464..4206e4c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { ToolbarComponent } from './components/toolbar/toolbar.component'; import { ThemeService } from './services/theme.service'; -import { ChallengeWebSocketService } from './services/challenge-websocket.service'; @Component({ selector: 'app-root', @@ -11,13 +10,9 @@ import { ChallengeWebSocketService } from './services/challenge-websocket.servic styleUrl: './app.css' }) export class App implements OnInit { - constructor( - private readonly themeService: ThemeService, - private readonly challengeWs: ChallengeWebSocketService - ) { } + constructor(private readonly themeService: ThemeService) { } ngOnInit(): void { this.themeService.initTheme(); - this.challengeWs.connect(); } } diff --git a/src/app/core/config.loader.ts b/src/app/core/config.loader.ts new file mode 100644 index 0000000..2d10fd5 --- /dev/null +++ b/src/app/core/config.loader.ts @@ -0,0 +1,11 @@ +/** + * Load runtime configuration from window.__RUNTIME_CONFIG__ + * This is injected by docker-entrypoint.sh at container startup + */ +export function loadRuntimeConfig() { + const config = (window as any).__RUNTIME_CONFIG__ || {}; + return { + apiUrl: config.API_URL || '', + wsUrl: config.WEBSOCKET_URL || 'ws://localhost:8080' + }; +} diff --git a/src/app/pages/welcome/welcome.component.html b/src/app/pages/welcome/welcome.component.html index 98509a7..487018a 100644 --- a/src/app/pages/welcome/welcome.component.html +++ b/src/app/pages/welcome/welcome.component.html @@ -138,6 +138,14 @@ + + @if (showSpeechBubble) { +
+
+
{{ bubbleMessage }}
+
+
+ @if (showSpeechBubble) {
@@ -148,6 +156,39 @@
} + + @if (isZoomedIn) { +
+
+
+
+ Player 2 + @if (showSecondSpeechBubble) { +
+
Feed me! 🍖
+
+
+ } + @if (showHappyBubble) { +
+
Happy meow! 😸
+
+
+ } +
+
+ + + @if (showMeatEmoji) { +
+ 🍖 +
+ } +
+ } + @if (isZoomedIn) {
+ @if (showDifficultyDialog) { +
+
+
SELECT DIFFICULTY
+
+ + + + } + +
+
+
+ @if (showDifficultyDialog) {
@@ -261,6 +316,10 @@ } + @if (errorMessage) { +

{{ errorMessage }}

+ } +
@if (errorMessage) {

{{ errorMessage }}

} diff --git a/src/app/pages/welcome/welcome.component.ts b/src/app/pages/welcome/welcome.component.ts index 31f8d27..bac297f 100644 --- a/src/app/pages/welcome/welcome.component.ts +++ b/src/app/pages/welcome/welcome.component.ts @@ -1,13 +1,18 @@ import { CommonModule } from '@angular/common'; import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { finalize } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getErrorMessage } from '../../core/http/error-message.util'; import { CurrentUser } from '../../models/auth.models'; import { AuthDialogService } from '../../services/auth-dialog.service'; import { AuthService } from '../../services/auth.service'; +import { CurrentUser } from '../../models/auth.models'; +import { AuthDialogService } from '../../services/auth-dialog.service'; +import { AuthService } from '../../services/auth.service'; import { GameApiService } from '../../services/game-api.service'; import { ThemeService } from '../../services/theme.service'; import { ChallengeCreateDialogComponent } from '../../components/challenge-create-dialog/challenge-create-dialog.component'; @@ -36,7 +41,14 @@ interface WindowCell { imports: [CommonModule, FormsModule, ChallengeCreateDialogComponent], templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.css'] + styleUrls: ['./welcome.component.css'] }) +export class WelcomeComponent implements OnInit, OnDestroy { + private readonly destroyRef = inject(DestroyRef); + private readonly authService = inject(AuthService); + private readonly authDialogService = inject(AuthDialogService); + private readonly themeService = inject(ThemeService); + export class WelcomeComponent implements OnInit, OnDestroy { private readonly destroyRef = inject(DestroyRef); private readonly authService = inject(AuthService); @@ -49,6 +61,12 @@ export class WelcomeComponent implements OnInit, OnDestroy { showChallengeDialog = false; errorMessage = ''; + showDifficultyDialog = false; + showOptionsDialog = false; + showJoinDialog = false; + showImportDialog = false; + + showDifficultyDialog = false; showOptionsDialog = false; showJoinDialog = false; @@ -90,6 +108,43 @@ export class WelcomeComponent implements OnInit, OnDestroy { private coolColors = ['#7de8ff', '#00d5ff', '#5bc0de', '#31b0d5', '#4fc3f7', '#29b6f6']; private coolGlowColors = ['#00d5ff', '#00d5ff', '#31b0d5', '#31b0d5', '#03a9f4', '#0288d1']; + private warmColors = ['#ffe88a', '#ffcc30', '#f0ad4e', '#ec971f', '#ffb74d', '#ffa726']; + private warmGlowColors = ['#ffcc30', '#ffcc30', '#ec971f', '#ec971f', '#ff9800', '#fb8c00']; + importMode: ImportMode = 'fen'; + importText = ''; + + isSunsetMode = false; + modeBadge = 'NIGHT MODE'; + currentUser: CurrentUser | null = null; + private authDialogState: 'login' | 'register' | null = null; + private pendingAction: (() => void) | null = null; + + // Speech bubble and zoom features + showSpeechBubble = false; + isZoomedIn = false; + showSecondSpeechBubble = false; + showHappyBubble = false; + showMeatEmoji = false; + bubbleMessage = 'meow'; + + // Meat emoji drag state + meatX = 0; + meatY = 0; + isDraggingMeat = false; + meatDragOffsetX = 0; + meatDragOffsetY = 0; + + stars: Star[] = []; + bgBuildings: BackgroundBuilding[] = []; + windows: Record = {}; + + private flickerIntervalId: ReturnType | undefined; + private speechBubbleTimeoutId: ReturnType | undefined; + private zoomTimeoutId: ReturnType | undefined; + + private coolColors = ['#7de8ff', '#00d5ff', '#5bc0de', '#31b0d5', '#4fc3f7', '#29b6f6']; + private coolGlowColors = ['#00d5ff', '#00d5ff', '#31b0d5', '#31b0d5', '#03a9f4', '#0288d1']; + private warmColors = ['#ffe88a', '#ffcc30', '#f0ad4e', '#ec971f', '#ffb74d', '#ffa726']; private warmGlowColors = ['#ffcc30', '#ffcc30', '#ec971f', '#ec971f', '#ff9800', '#fb8c00']; @@ -352,6 +407,136 @@ export class WelcomeComponent implements OnInit, OnDestroy { action(); } + private performStartOneVsOne(): void { + if (!this.requireAuth(() => this.performStartOneVsOne())) { + return; + } + + this.performStartOneVsOne(); + } + + startVsBot(difficulty: Difficulty): void { + if (!this.requireAuth(() => this.performStartVsBot(difficulty))) { + return; + } + + this.performStartVsBot(difficulty); + } + + submitJoinGame(): void { + if (!this.requireAuth(() => this.performSubmitJoinGame())) { + return; + } + + this.performSubmitJoinGame(); + } + + submitImportGame(): void { + if (!this.requireAuth(() => this.performSubmitImportGame())) { + return; + } + + this.performSubmitImportGame(); + } + + onSpeechBubbleClick(): void { + this.showSpeechBubble = false; + this.isZoomedIn = true; + this.bubbleMessage = 'meow'; + this.showMeatEmoji = true; + this.showHappyBubble = false; + this.showSecondSpeechBubble = true; + + // Reset meat position + this.meatX = window.innerWidth / 2 - 100; + this.meatY = window.innerHeight / 2 + 150; + } + + onZoomedViewClick(): void { + this.isZoomedIn = false; + this.showSecondSpeechBubble = false; + this.showHappyBubble = false; + this.showMeatEmoji = false; + this.bubbleMessage = 'meow'; + + if (this.zoomTimeoutId) { + clearTimeout(this.zoomTimeoutId); + } + } + + onMeatMouseDown(event: MouseEvent): void { + this.isDraggingMeat = true; + const rect = (event.target as HTMLElement).getBoundingClientRect(); + this.meatDragOffsetX = event.clientX - rect.left; + this.meatDragOffsetY = event.clientY - rect.top; + } + + onMouseMove(event: MouseEvent): void { + if (!this.isDraggingMeat) { + return; + } + + this.meatX = event.clientX - this.meatDragOffsetX; + this.meatY = event.clientY - this.meatDragOffsetY; + + const gifElement = document.querySelector('.player-2-gif') as HTMLElement; + if (!gifElement) { + return; + } + + const gifRect = gifElement.getBoundingClientRect(); + const gifCenterX = gifRect.left + gifRect.width / 2; + const gifCenterY = gifRect.top + gifRect.height / 2; + + const meatElement = document.querySelector('.meat-emoji') as HTMLElement; + if (!meatElement) { + return; + } + + const meatRect = meatElement.getBoundingClientRect(); + const meatCenterX = meatRect.left + meatRect.width / 2; + const meatCenterY = meatRect.top + meatRect.height / 2; + + const distance = Math.sqrt( + Math.pow(meatCenterX - gifCenterX, 2) + Math.pow(meatCenterY - gifCenterY, 2) + ); + + if (distance < 50) { + this.onMeatFed(); + } + } + + onMouseUp(): void { + this.isDraggingMeat = false; + } + + onMeatFed(): void { + this.showMeatEmoji = false; + this.showSecondSpeechBubble = false; + this.showHappyBubble = true; + this.isDraggingMeat = false; + } + + private requireAuth(action: () => void): boolean { + if (this.authService.isLoggedIn()) { + return true; + } + + this.pendingAction = action; + this.authDialogService.openLogin(); + return false; + } + + private maybeRunPendingAction(): void { + if (!this.currentUser || this.authDialogState !== null || !this.pendingAction) { + return; + } + + const action = this.pendingAction; + this.pendingAction = null; + action(); + } + private performStartOneVsOne(): void { if (this.creating) { return; @@ -368,6 +553,9 @@ export class WelcomeComponent implements OnInit, OnDestroy { void this.router.navigate(['/game', game.gameId], { state: { theme: this.isSunsetMode ? 'light' : 'dark' } }); + void this.router.navigate(['/game', game.gameId], { + state: { theme: this.isSunsetMode ? 'light' : 'dark' } + }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to create a game.'); @@ -375,6 +563,7 @@ export class WelcomeComponent implements OnInit, OnDestroy { }); } + private performStartVsBot(difficulty: Difficulty): void { private performStartVsBot(difficulty: Difficulty): void { if (this.creating) { return; @@ -383,6 +572,7 @@ export class WelcomeComponent implements OnInit, OnDestroy { this.errorMessage = ''; this.creating = true; this.showDifficultyDialog = false; + this.showDifficultyDialog = false; this.gameApi .createGameVsBot(difficulty) @@ -392,6 +582,9 @@ export class WelcomeComponent implements OnInit, OnDestroy { void this.router.navigate(['/game', game.gameId], { state: { theme: this.isSunsetMode ? 'light' : 'dark' } }); + void this.router.navigate(['/game', game.gameId], { + state: { theme: this.isSunsetMode ? 'light' : 'dark' } + }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to create a game against bot.'); @@ -399,6 +592,9 @@ export class WelcomeComponent implements OnInit, OnDestroy { }); } + private performSubmitJoinGame(): void { + const gameId = this.gameIdInput.trim(); + if (this.joiningGame || !gameId) { private performSubmitJoinGame(): void { const gameId = this.gameIdInput.trim(); if (this.joiningGame || !gameId) { @@ -409,6 +605,7 @@ export class WelcomeComponent implements OnInit, OnDestroy { this.joiningGame = true; this.gameApi + .getGame(gameId) .getGame(gameId) .pipe(finalize(() => (this.joiningGame = false))) .subscribe({ @@ -417,6 +614,10 @@ export class WelcomeComponent implements OnInit, OnDestroy { void this.router.navigate(['/game', game.gameId], { state: { theme: this.isSunsetMode ? 'light' : 'dark' } }); + this.closeJoinDialog(); + void this.router.navigate(['/game', game.gameId], { + state: { theme: this.isSunsetMode ? 'light' : 'dark' } + }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to find or join the game.'); @@ -573,6 +774,159 @@ export class WelcomeComponent implements OnInit, OnDestroy { private stopWindowFlicker(): void { if (this.flickerIntervalId === undefined) { return; + private generateStars(count: number): void { + this.stars = Array.from({ length: count }, () => { + const size = Math.random() * 2 + 0.5; + return { + style: { + width: `${size}px`, + height: `${size}px`, + left: `${Math.random() * 100}%`, + top: `${Math.random() * 62}%`, + '--d': `${(Math.random() * 3 + 1.5).toFixed(1)}s`, + '--dl': `${-(Math.random() * 6).toFixed(1)}s` + } + }; + }); + } + + private generateBackgroundBuildings(): void { + const specs = [ + { l: '0%', w: '7%', h: '30vh' }, + { l: '3%', w: '4%', h: '18vh' }, // New building + { l: '7%', w: '5%', h: '22vh' }, + { l: '11%', w: '8%', h: '28vh' }, + { l: '15%', w: '6%', h: '20vh' }, + { l: '18.5%', w: '4%', h: '18vh' }, + { l: '22.5%', w: '6%', h: '26vh' }, + { l: '28%', w: '5%', h: '25vh' }, + { l: '32%', w: '4%', h: '15vh' }, + { l: '35.5%', w: '4.5%', h: '20vh' }, + { l: '42%', w: '5%', h: '28vh' }, + { l: '47%', w: '5%', h: '22vh' }, // New building + { l: '50%', w: '7%', h: '30vh' }, + { l: '55%', w: '6%', h: '27vh' }, + { l: '60.5%', w: '5%', h: '24vh' }, + { l: '64.5%', w: '3.5%', h: '17vh' }, + { l: '70%', w: '6%', h: '23vh' }, + { l: '75%', w: '4%', h: '19vh' }, + { l: '80.5%', w: '4%', h: '21vh' }, + { l: '85.5%', w: '9%', h: '32vh' }, + { l: '88%', w: '5%', h: '20vh' }, + { l: '91%', w: '3%', h: '16vh' }, // New building + { l: '94%', w: '6%', h: '27vh' } + ]; + + this.bgBuildings = specs.map((spec) => ({ + style: { left: spec.l, width: spec.w, height: spec.h } + })); + } + + private generateWindowsForAllBuildings(): void { + this.windows = { + wA1: this.generateWindows(3, 4, 0.6), + wA2: this.generateWindows(4, 5, 0.55), + wA3: this.generateWindows(5, 18, 0.5), + wB1: this.generateWindows(4, 3, 0.6), + wB2: this.generateWindows(5, 20, 0.55), + wC1: this.generateWindows(5, 3, 0.7), + wC2: this.generateWindows(6, 5, 0.65), + wC3: this.generateWindows(7, 24, 0.6), + wD1: this.generateWindows(6, 3, 0.6), + wD2: this.generateWindows(6, 20, 0.5), + wE1: this.generateWindows(3, 16, 0.45) + }; + } + + private generateWindows(cols: number, rows: number, litRate: number): WindowCell[] { + const total = cols * rows; + return Array.from({ length: total }, () => this.createWindowCell(litRate)); + } + + private createWindowCell(litRate: number): WindowCell { + const random = Math.random(); + let state: WindowCell['state'] = 'off'; + let color: string | undefined; + let glowColor: string | undefined; + + if (random < litRate * 0.58) { // Cool color + state = 'on'; + const coolIndex = Math.floor(Math.random() * this.coolColors.length); + color = this.coolColors[coolIndex]; + glowColor = this.coolGlowColors[coolIndex]; + } else if (random < litRate) { // Warm color + state = 'on'; + const warmIndex = Math.floor(Math.random() * this.warmColors.length); + color = this.warmColors[warmIndex]; + glowColor = this.warmGlowColors[warmIndex]; + } + + if (state === 'off') { + return { state, style: {} }; + } + + const baseDuration = (color && this.coolColors.includes(color)) ? 3 : 4; + return { + state, + color, + glowColor, + style: { + 'background-color': color || '', + 'box-shadow': glowColor ? `0 0 6px ${glowColor}, 0 0 16px ${glowColor}35` : '', + '--wd': `${(Math.random() * 4 + baseDuration).toFixed(1)}s`, + '--wdl': `${-(Math.random() * 8).toFixed(1)}s` + } + }; + } + + private startWindowFlicker(): void { + this.flickerIntervalId = setInterval(() => { + this.randomFlicker(); + }, 2800); + } + + private stopWindowFlicker(): void { + if (this.flickerIntervalId === undefined) { + return; + } + clearInterval(this.flickerIntervalId); + this.flickerIntervalId = undefined; + } + + private randomFlicker(): void { + const allWindows = Object.values(this.windows).flat(); + if (allWindows.length === 0) { + return; + } + + const pickCount = Math.floor(Math.random() * 6) + 1; + for (let i = 0; i < pickCount; i += 1) { + const target = allWindows[Math.floor(Math.random() * allWindows.length)]; + if (!target) { + continue; + } + + if (target.state === 'off') { + target.state = 'on'; + const isCool = Math.random() < 0.5; + const colors = isCool ? this.coolColors : this.warmColors; + const glowColors = isCool ? this.coolGlowColors : this.warmGlowColors; + const index = Math.floor(Math.random() * colors.length); + + target.color = colors[index]; + target.glowColor = glowColors[index]; + target.style = { + 'background-color': target.color || '', + 'box-shadow': target.glowColor ? `0 0 6px ${target.glowColor}, 0 0 16px ${target.glowColor}35` : '', + '--wd': `${(Math.random() * 4 + (isCool ? 3 : 4)).toFixed(1)}s`, + '--wdl': `${-(Math.random() * 8).toFixed(1)}s` + }; + } else { + target.state = 'off'; + target.color = undefined; + target.glowColor = undefined; + target.style = {}; + } } clearInterval(this.flickerIntervalId); this.flickerIntervalId = undefined; diff --git a/src/environments/environment.staging.ts b/src/environments/environment.staging.ts new file mode 100644 index 0000000..a2f73b6 --- /dev/null +++ b/src/environments/environment.staging.ts @@ -0,0 +1,11 @@ +import { loadRuntimeConfig } from '../app/core/config.loader'; + +const runtimeConfig = loadRuntimeConfig(); + +export const environment = { + production: true, + apiBaseUrl: runtimeConfig.apiUrl || 'https://st.nowchess.janis-eccarius.de', + accountServiceUrl: runtimeConfig.apiUrl || 'https://st.nowchess.janis-eccarius.de', + wsBaseUrl: runtimeConfig.wsUrl || 'wss://st.nowchess.janis-eccarius.de', + apiPath: '/api/board/game' +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 8c38733..2478e38 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,7 +1,11 @@ +import { loadRuntimeConfig } from '../app/core/config.loader'; + +const runtimeConfig = loadRuntimeConfig(); + export const environment = { - production: true, - apiBaseUrl: '', - accountServiceUrl: '', - wsBaseUrl: 'ws://localhost:8080', + production: false, + apiBaseUrl: runtimeConfig.apiUrl || '', + accountServiceUrl: runtimeConfig.apiUrl || '', + wsBaseUrl: runtimeConfig.wsUrl, apiPath: '/api/board/game' }; diff --git a/src/index.html b/src/index.html index 5f83c38..1d5f35e 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,7 @@ + diff --git a/versions.env b/versions.env new file mode 100644 index 0000000..0e288f2 --- /dev/null +++ b/versions.env @@ -0,0 +1,3 @@ +MAJOR=0 +MINOR=1 +PATCH=0