32 Commits

Author SHA1 Message Date
shahdlala66 7de4f3784b fix: path problem
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 09:19:03 +02:00
shahdlala66 04df2250e9 feat: dockerfile added 2026-05-06 09:07:33 +02:00
shahdlala66 99100a3086 fix: no more build errors 2026-05-06 08:59:36 +02:00
Lala, Shahd e3466bda30 feat: game starts after accept 2026-05-05 22:25:49 +00:00
Lala, Shahd 550db1401b feat: challange request, accept, decline, page, added (not complete, polling issues, and not stable, and too much code for nothing) 2026-05-05 22:14:45 +00:00
Lala, Shahd 6a79be45bf feat: challange request, accept, decline, page, added (not complete, polling issues, and not stable, and too much code for nothing)
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 22:14:20 +00:00
Lala, Shahd 82bf006f18 fix: proxy issues
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 19:42:27 +00:00
Lala, Shahd 13edcb2f69 chore(merge): sync main into feat-ncs-66 2026-05-05 19:24:14 +00:00
Lala, Shahd 49d6aae1db feat: copy username 2026-05-05 18:52:12 +00:00
Lala, Shahd eadcd770ba feat: profile info view (player ID, rating etc) 2026-05-04 21:43:12 +00:00
shahdlala66 a3255602b3 feat: me tab added 2026-05-04 19:48:55 +02:00
Lala, Shahd d471eef7af feat: abstract buttons 2026-05-04 09:56:09 +02:00
Lala, Shahd 361ce1e817 feat: login and register, style is not ready 2026-05-03 20:49:08 +00:00
Lala, Shahd aa70083aed feat: login and register, style is not ready 2026-05-03 20:44:01 +00:00
shahdlala66 3b757d7ff7 feat: added a cat not sure about it tho 2026-04-28 22:45:37 +02:00
shahdlala66 19f3359106 style: more colors for windows 2026-04-26 22:13:33 +02:00
shahdlala66 d676288315 style: changed colors etc 2026-04-26 22:13:26 +02:00
shahdlala66 671886781e feat: new boared design 2026-04-26 13:34:55 +02:00
shahdlala66 dc9a7b2e32 feat: drag and drop 2026-04-22 13:16:44 +02:00
shahdlala66 5951257c99 feat: move history, export import fixed, timer added 2026-04-22 13:05:09 +02:00
shahdlala66 a59e2a023b style: bigger gifs 2026-04-22 09:31:15 +02:00
shahdlala66 4f76bcc7c6 feat: piece pormotion 2026-04-22 08:35:58 +02:00
shahdlala66 25b69fd7b6 feat: clean ups and shorter files 2026-04-22 08:28:16 +02:00
shahdlala66 c18026bce6 feat: added dark and light mode 2026-04-22 08:19:16 +02:00
shahdlala66 91fa247696 style: added new gifs (optianl) 2026-04-21 15:12:41 +02:00
shahdlala66 97365371c8 feat: new spec 2026-04-21 13:40:48 +02:00
shahdlala66 fdc0f1d73b feat: join pervious game added 2026-04-19 11:14:14 +02:00
shahdlala66 bc644c16e3 feat: 1vs BOT 2026-04-19 01:06:13 +02:00
shahdlala66 5497997455 feat: more components 2026-04-19 01:00:14 +02:00
shahdlala66 53459648c6 style: styles file added 2026-04-19 00:24:43 +02:00
shahdlala66 8c97a726a7 fix: removed repetitve code 2026-04-19 00:12:13 +02:00
shahdlala66 2582f8e4d6 style: brought back old style for text input 2026-04-19 00:05:52 +02:00
21 changed files with 329 additions and 225 deletions
-40
View File
@@ -1,40 +0,0 @@
## 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))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.1.0...0.0.0) (2026-05-12)
### Features
* NCS-69 Challenge request ([#3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/3)) ([bad7366](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bad7366bdbb048c20218257b30ac22efc9ecb6db))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.0...0.0.0) (2026-05-12)
### Bug Fixes
* NCWF-1 401 ([#5](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/5)) ([f8f93ef](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/f8f93efff48f1d7198023fed45b675c2e225df36))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.1...0.0.0) (2026-05-12)
### Bug Fixes
* NCWF-1 401 ([#6](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/6)) ([6d1e06d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/6d1e06dfd606b93d029e9c9b84eea6f8b3b6294e))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.2...0.0.0) (2026-05-14)
### Bug Fixes
* added missing challenge routes ([61000f8](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/61000f8a22aff8b524664a756cc933834365f923))
+6 -12
View File
@@ -1,25 +1,19 @@
FROM --platform=$BUILDPLATFORM node:lts-alpine3.23 AS builder
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --silent
RUN npm install
COPY . .
RUN npm run build
FROM --platform=$TARGETPLATFORM nginx:stable-alpine AS production
RUN npm run build -- --configuration production
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
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
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
COPY --from=build /app/dist/nowchess-frontend/browser /usr/share/nginx/html
EXPOSE 80
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+4 -15
View File
@@ -47,25 +47,17 @@
"budgets": [
{
"type": "initial",
"maximumWarning": "1MB",
"maximumError": "5MB"
"maximumWarning": "500kB",
"maximumError": "4MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "1MB",
"maximumError": "5MB"
"maximumWarning": "1.5MB",
"maximumError": "2MB"
}
],
"outputHashing": "all"
},
"staging": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
]
},
"development": {
"fileReplacements": [
{
@@ -91,9 +83,6 @@
"production": {
"buildTarget": "nowchess-frontend:build:production"
},
"staging": {
"buildTarget": "nowchess-frontend:build:staging"
},
"development": {
"buildTarget": "nowchess-frontend:build:development"
}
-18
View File
@@ -1,18 +0,0 @@
#!/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;'
+12 -18
View File
@@ -1,22 +1,16 @@
server {
listen 80;
server_name localhost;
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location / {
try_files $uri $uri/ /index.html;
}
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;
}
location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public";
}
}
+1 -2
View File
@@ -4,8 +4,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --configuration production",
"build:staging": "ng build --configuration staging",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
-4
View File
@@ -1,4 +0,0 @@
window.__RUNTIME_CONFIG__ = {
API_URL: "${API_URL}",
WEBSOCKET_URL: "${WEBSOCKET_URL}"
};
+6 -1
View File
@@ -2,6 +2,7 @@ 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',
@@ -10,9 +11,13 @@ import { ThemeService } from './services/theme.service';
styleUrl: './app.css'
})
export class App implements OnInit {
constructor(private readonly themeService: ThemeService) { }
constructor(
private readonly themeService: ThemeService,
private readonly challengeWs: ChallengeWebSocketService
) { }
ngOnInit(): void {
this.themeService.initTheme();
this.challengeWs.connect();
}
}
@@ -82,3 +82,148 @@
.ms-auto {
margin-left: auto;
}
.notification-container {
position: relative;
}
.notification-badge {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
position: relative;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
&.has-notifications {
animation: pulse 2s infinite;
}
.badge {
position: absolute;
top: -8px;
right: -8px;
background-color: #ff6b6b;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}
}
@keyframes pulse {
0% {
filter: drop-shadow(0 0 3px rgba(0, 213, 255, 0.3));
}
50% {
filter: drop-shadow(0 0 8px rgba(0, 213, 255, 0.6));
}
100% {
filter: drop-shadow(0 0 3px rgba(0, 213, 255, 0.3));
}
}
.notification-menu {
position: absolute;
top: 100%;
right: 0;
background: linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%);
border: 1px solid rgba(0, 213, 255, 0.3);
border-radius: 8px;
min-width: 250px;
max-height: 300px;
overflow-y: auto;
z-index: 1001;
box-shadow: 0 0 20px rgba(0, 213, 255, 0.3);
margin-top: 10px;
.notification-menu-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid rgba(0, 213, 255, 0.2);
color: #00d5ff;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
.close-btn {
background: none;
border: none;
color: #00d5ff;
font-size: 20px;
cursor: pointer;
padding: 0;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
}
.notification-list {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
}
.notification-item {
padding: 10px 12px;
background-color: rgba(0, 213, 255, 0.05);
border: 1px solid rgba(0, 213, 255, 0.1);
border-radius: 4px;
font-size: 12px;
color: #b0b0d0;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: rgba(0, 213, 255, 0.1);
border-color: rgba(0, 213, 255, 0.3);
}
}
.notification-menu-footer {
padding: 8px 8px 0;
border-top: 1px solid rgba(0, 213, 255, 0.2);
}
.view-all-btn {
width: 100%;
padding: 8px 12px;
background-color: rgba(0, 213, 255, 0.1);
border: 1px solid rgba(0, 213, 255, 0.3);
border-radius: 4px;
color: #00d5ff;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: rgba(0, 213, 255, 0.2);
border-color: #00d5ff;
}
}
}
@@ -3,6 +3,37 @@
<span class="navbar-brand">NowChess</span>
<div class="ms-auto">
<div class="d-flex align-items-center gap-2">
<!-- Challenge Notification Badge -->
<div class="notification-container">
<button type="button" class="notification-badge" (click)="toggleNotificationMenu()"
[class.has-notifications]="incomingChallenges.length > 0" title="View challenges">
🔔
<span *ngIf="incomingChallenges.length > 0" class="badge">
{{ incomingChallenges.length }}
</span>
</button>
<!-- Notification Menu Dropdown -->
@if (showNotificationMenu && incomingChallenges.length > 0) {
<div class="notification-menu" (click)="$event.stopPropagation()">
<div class="notification-menu-header">
Challenges
<button type="button" class="close-btn" (click)="closeNotificationMenu()">×</button>
</div>
<div class="notification-list">
<div *ngFor="let challenge of incomingChallenges" class="notification-item">
{{ challenge.challenger.name }}
</div>
</div>
<div class="notification-menu-footer">
<button type="button" class="view-all-btn" (click)="goToChallenges()">
View All Challenges →
</button>
</div>
</div>
}
</div>
<button type="button" class="app-btn" (click)="toggleTheme()">
{{ isDarkMode ? 'Light mode' : 'Dark mode' }}
</button>
@@ -11,6 +42,9 @@
<button type="button" class="me-btn" (click)="goToProfile()">
👤 {{ user.username }}
</button>
<button type="button" class="app-btn" (click)="goToChallenges()">
Challenges
</button>
<button type="button" class="app-btn" (click)="logout()">Logout</button>
</div>
} @else {
@@ -26,6 +60,12 @@
</div>
</nav>
<!-- Challenge Notification Popup -->
@if (displayedChallenge) {
<app-challenge-notification [challenge]="displayedChallenge" (accept)="onChallengeAccepted($event)"
(decline)="onChallengeDeclined($event)" (close)="onNotificationClose()" />
}
@if (showLoginDialog) {
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
}
@@ -8,11 +8,14 @@ import { CurrentUser } from '../../models/auth.models';
import { LoginDialogComponent } from '../login-dialog/login-dialog.component';
import { RegisterDialogComponent } from '../register-dialog/register-dialog.component';
import { ThemeService } from '../../services/theme.service';
import { ChallengeEventService } from '../../services/challenge-event.service';
import { ChallengeNotificationComponent } from '../challenge-notification/challenge-notification.component';
import { Challenge } from '../../models/challenge.models';
@Component({
selector: 'app-toolbar',
standalone: true,
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent],
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent, ChallengeNotificationComponent],
templateUrl: './toolbar.component.html',
styleUrl: './toolbar.component.css'
})
@@ -21,12 +24,16 @@ export class ToolbarComponent implements OnInit {
private readonly authService = inject(AuthService);
private readonly authDialogService = inject(AuthDialogService);
private readonly themeService = inject(ThemeService);
private readonly challengeEventService = inject(ChallengeEventService);
private readonly router = inject(Router);
currentUser: CurrentUser | null = null;
showLoginDialog = false;
showRegisterDialog = false;
isDarkMode = false;
incomingChallenges: Challenge[] = [];
showNotificationMenu = false;
displayedChallenge: Challenge | null = null;
ngOnInit(): void {
this.authService.currentUser$
@@ -47,6 +54,22 @@ export class ToolbarComponent implements OnInit {
.subscribe((isDarkMode) => {
this.isDarkMode = isDarkMode;
});
this.challengeEventService.getIncomingChallenges$()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((challenges) => {
this.incomingChallenges = challenges;
// Show the most recent challenge as notification
if (challenges.length > 0) {
this.displayedChallenge = challenges[0];
}
});
this.challengeEventService.getChallengeReceived$()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((challenge) => {
this.displayedChallenge = challenge;
});
}
openLoginDialog(): void {
@@ -84,4 +107,34 @@ export class ToolbarComponent implements OnInit {
onRegisterSuccess(): void {
this.closeRegisterDialog();
}
toggleNotificationMenu(): void {
this.showNotificationMenu = !this.showNotificationMenu;
}
closeNotificationMenu(): void {
this.showNotificationMenu = false;
}
onChallengeAccepted(challenge: Challenge): void {
this.challengeEventService.onChallengeAccepted(challenge);
this.displayedChallenge = null;
this.closeNotificationMenu();
// Navigate to the game (once game creation is handled by backend)
}
onChallengeDeclined(challenge: Challenge): void {
this.challengeEventService.removeChallenge(challenge.id);
this.displayedChallenge = null;
this.closeNotificationMenu();
}
onNotificationClose(): void {
this.displayedChallenge = null;
}
goToChallenges(): void {
this.closeNotificationMenu();
void this.router.navigate(['/challenges']);
}
}
-11
View File
@@ -1,11 +0,0 @@
/**
* 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'
};
}
+4 -14
View File
@@ -46,13 +46,6 @@
<div class="w" *ngFor="let win of windows['wA3']" [ngStyle]="win.style"></div>
</div>
</div>
<!-- Draggable Meat Emoji -->
@if (showMeatEmoji) {
<div class="meat-emoji" [style.left.px]="meatX" [style.top.px]="meatY" (mousedown)="onMeatMouseDown($event)">
🍖
</div>
}
</div>
<div class="bwrap" style="left:21%;width:15%;">
@@ -101,8 +94,8 @@
<div class="bb-tag">WELCOME</div>
<div class="bb-title" style="font-size:clamp(16px,1.8vw,26px);">WELCOME TO<br />NOWCHESS</div>
<div class="bb-subtitle">Play your next move from the skyline.</div>
<button type="button" class="app-btn" (click)="startOneVsOne()" [disabled]="creating">
{{ creating ? 'CREATING...' : 'START NOW →' }}
<button type="button" class="app-btn" (click)="openChallengeDialog()" [disabled]="creating">
{{ creating ? 'CREATING...' : 'CREATE GAME →' }}
</button>
</div>
</div>
@@ -211,6 +204,7 @@
<div class="dialog-card" (click)="$event.stopPropagation()">
<div class="dialog-title">MORE OPTIONS</div>
<div class="dialog-actions">
<button type="button" class="app-btn" (click)="openChallengeDialog()">START GAME</button>
<button type="button" class="app-btn" (click)="openImportDialog()">IMPORT GAME</button>
</div>
</div>
@@ -264,11 +258,7 @@
}
@if (showChallengeDialog) {
<div class="dialog-overlay" (click)="closeChallengeDialog()">
<div class="dialog-card" (click)="$event.stopPropagation()">
<app-challenge-create-dialog (closeChallengeDialog)="closeChallengeDialog()"></app-challenge-create-dialog>
</div>
</div>
<app-challenge-create-dialog (closeChallengeDialog)="closeChallengeDialog()"></app-challenge-create-dialog>
}
@if (errorMessage) {
+40 -28
View File
@@ -30,21 +30,6 @@ interface WindowCell {
style: Record<string, string>;
}
interface Star {
style: Record<string, string>;
}
interface BackgroundBuilding {
style: Record<string, string>;
}
interface WindowCell {
state: 'off' | 'on';
color?: string;
glowColor?: string;
style: Record<string, string>;
}
@Component({
selector: 'app-welcome',
standalone: true,
@@ -61,13 +46,13 @@ export class WelcomeComponent implements OnInit, OnDestroy {
creating = false;
joiningGame = false;
importing = false;
showChallengeDialog = false;
errorMessage = '';
showDifficultyDialog = false;
showOptionsDialog = false;
showJoinDialog = false;
showImportDialog = false;
showChallengeDialog = false;
gameIdInput = '';
importMode: ImportMode = 'fen';
@@ -171,6 +156,20 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.errorMessage = '';
}
openChallengeDialog(): void {
if (!this.requireAuth(() => this.showChallengeDialog = true)) {
return;
}
this.closeAllDialogs();
this.showChallengeDialog = true;
}
closeChallengeDialog(): void {
this.showChallengeDialog = false;
this.errorMessage = '';
}
openOptionsDialog(): void {
this.closeAllDialogs();
this.showOptionsDialog = true;
@@ -224,21 +223,11 @@ export class WelcomeComponent implements OnInit, OnDestroy {
}
startOneVsOne(): void {
if (!this.requireAuth(() => this.openChallengeDialog())) {
if (!this.requireAuth(() => this.performStartOneVsOne())) {
return;
}
this.openChallengeDialog();
}
openChallengeDialog(): void {
this.closeAllDialogs();
this.showChallengeDialog = true;
}
closeChallengeDialog(): void {
this.showChallengeDialog = false;
this.errorMessage = '';
this.performStartOneVsOne();
}
startVsBot(difficulty: Difficulty): void {
@@ -363,6 +352,28 @@ export class WelcomeComponent implements OnInit, OnDestroy {
action();
}
private performStartOneVsOne(): void {
if (this.creating) {
return;
}
this.errorMessage = '';
this.creating = true;
this.gameApi
.createGame()
.pipe(finalize(() => (this.creating = false)))
.subscribe({
next: (game) => {
void this.router.navigate(['/game', game.gameId], {
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
});
},
error: (error) => {
this.errorMessage = getErrorMessage(error, 'Unable to create a game.');
}
});
}
private performStartVsBot(difficulty: Difficulty): void {
if (this.creating) {
@@ -444,6 +455,7 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.showOptionsDialog = false;
this.showJoinDialog = false;
this.showImportDialog = false;
this.showChallengeDialog = false;
this.errorMessage = '';
}
+1 -1
View File
@@ -3,11 +3,11 @@ import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token');
// Add token to protected endpoints only (not registration or login)
const isProtectedEndpoint =
req.url.includes('/api/account/me') ||
req.url.includes('/api/account/bots') ||
req.url.includes('/api/account/official-bots') ||
req.url.includes('/api/board/game') ||
req.url.includes('/api/challenge');
if (token && isProtectedEndpoint) {
+7 -17
View File
@@ -10,7 +10,6 @@ export class GameStreamService {
private readonly destroyRef = inject(DestroyRef);
private streamSubscription: Subscription | null = null;
private pollSubscription: Subscription | null = null;
private lastGameStateHash: string | null = null;
startStreaming(
gameId: string,
@@ -21,10 +20,7 @@ export class GameStreamService {
.streamGame(gameId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (event) => {
this.lastGameStateHash = JSON.stringify(event);
onEvent(event);
},
next: (event) => onEvent(event),
error: () => {
onStreamError();
this.startPolling(gameId, onEvent);
@@ -41,7 +37,7 @@ export class GameStreamService {
return;
}
this.pollSubscription = interval(5000)
this.pollSubscription = interval(1500)
.pipe(
startWith(0),
switchMap(() => this.gameApi.getGame(gameId)),
@@ -49,16 +45,11 @@ export class GameStreamService {
)
.subscribe({
next: (game) => {
// Only emit if game state changed to avoid unnecessary updates
const stateHash = JSON.stringify(game.state);
if (this.lastGameStateHash !== stateHash) {
this.lastGameStateHash = stateHash;
const event: GameStreamEvent = {
type: 'gameFull',
game
};
onEvent(event);
}
const event: GameStreamEvent = {
type: 'gameFull',
game
};
onEvent(event);
}
});
}
@@ -68,6 +59,5 @@ export class GameStreamService {
this.pollSubscription?.unsubscribe();
this.streamSubscription = null;
this.pollSubscription = null;
this.lastGameStateHash = null;
}
}
+5 -20
View File
@@ -84,19 +84,8 @@ export class StreamHandlerService {
}
};
// Set timeout to fallback if WebSocket doesn't connect quickly
const connectionTimeoutId = setTimeout(() => {
if (!connected && !fallbackActive) {
console.warn(`[StreamHandler] WebSocket timeout for ${gameId}, attempting NDJSON fallback`);
ws.close();
void startNdjsonFallback();
}
}, 3000);
ws.onopen = () => {
connected = true;
clearTimeout(connectionTimeoutId);
console.log(`[StreamHandler] WebSocket connected for ${gameId}`);
};
ws.onmessage = (message) => {
@@ -108,23 +97,19 @@ export class StreamHandlerService {
};
ws.onerror = (error) => {
console.warn(`[StreamHandler] WebSocket error for ${gameId}:`, error);
clearTimeout(connectionTimeoutId);
if (!connected && !fallbackActive) {
console.warn(`[StreamHandler] WebSocket error for ${gameId}, attempting NDJSON fallback:`, error);
if (!connected) {
void startNdjsonFallback();
}
};
ws.onclose = () => {
clearTimeout(connectionTimeoutId);
console.warn(`[StreamHandler] WebSocket closed for ${gameId}, connected=${connected}`);
if (connected) {
// Connection was established but closed, stream is complete
observer.complete();
} else if (!fallbackActive) {
// Connection never established, try fallback
if (!connected) {
console.log(`[StreamHandler] Starting NDJSON fallback for ${gameId}`);
void startNdjsonFallback();
} else {
observer.complete();
}
};
-11
View File
@@ -1,11 +0,0 @@
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'
};
+4 -8
View File
@@ -1,11 +1,7 @@
import { loadRuntimeConfig } from '../app/core/config.loader';
const runtimeConfig = loadRuntimeConfig();
export const environment = {
production: false,
apiBaseUrl: runtimeConfig.apiUrl || '',
accountServiceUrl: runtimeConfig.apiUrl || '',
wsBaseUrl: runtimeConfig.wsUrl,
production: true,
apiBaseUrl: '',
accountServiceUrl: '',
wsBaseUrl: 'ws://localhost:8080',
apiPath: '/api/board/game'
};
-1
View File
@@ -6,7 +6,6 @@
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<script src="/env.js" defer></script>
</head>
<body>
<app-root></app-root>
-3
View File
@@ -1,3 +0,0 @@
MAJOR=0
MINOR=2
PATCH=3