Compare commits
33 Commits
0.2.2
..
8eb27ba8b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 8eb27ba8b9 | |||
| 7de4f3784b | |||
| 04df2250e9 | |||
| 99100a3086 | |||
| e3466bda30 | |||
| 550db1401b | |||
| 6a79be45bf | |||
| 82bf006f18 | |||
| 13edcb2f69 | |||
| 49d6aae1db | |||
| eadcd770ba | |||
| a3255602b3 | |||
| d471eef7af | |||
| 361ce1e817 | |||
| aa70083aed | |||
| 3b757d7ff7 | |||
| 19f3359106 | |||
| d676288315 | |||
| 671886781e | |||
| dc9a7b2e32 | |||
| 5951257c99 | |||
| a59e2a023b | |||
| 4f76bcc7c6 | |||
| 25b69fd7b6 | |||
| c18026bce6 | |||
| 91fa247696 | |||
| 97365371c8 | |||
| fdc0f1d73b | |||
| bc644c16e3 | |||
| 5497997455 | |||
| 53459648c6 | |||
| 8c97a726a7 | |||
| 2582f8e4d6 |
@@ -18,18 +18,3 @@
|
|||||||
* removed cache files ([7ee59c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/7ee59c434bf137a08fd560bbc02ceefbcfd90f04))
|
* 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))
|
* 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))
|
* 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))
|
|
||||||
|
|||||||
@@ -82,3 +82,148 @@
|
|||||||
.ms-auto {
|
.ms-auto {
|
||||||
margin-left: 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>
|
<span class="navbar-brand">NowChess</span>
|
||||||
<div class="ms-auto">
|
<div class="ms-auto">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<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()">
|
<button type="button" class="app-btn" (click)="toggleTheme()">
|
||||||
{{ isDarkMode ? 'Light mode' : 'Dark mode' }}
|
{{ isDarkMode ? 'Light mode' : 'Dark mode' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -11,6 +42,9 @@
|
|||||||
<button type="button" class="me-btn" (click)="goToProfile()">
|
<button type="button" class="me-btn" (click)="goToProfile()">
|
||||||
👤 {{ user.username }}
|
👤 {{ user.username }}
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="app-btn" (click)="goToChallenges()">
|
||||||
|
Challenges
|
||||||
|
</button>
|
||||||
<button type="button" class="app-btn" (click)="logout()">Logout</button>
|
<button type="button" class="app-btn" (click)="logout()">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -26,6 +60,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Challenge Notification Popup -->
|
||||||
|
@if (displayedChallenge) {
|
||||||
|
<app-challenge-notification [challenge]="displayedChallenge" (accept)="onChallengeAccepted($event)"
|
||||||
|
(decline)="onChallengeDeclined($event)" (close)="onNotificationClose()" />
|
||||||
|
}
|
||||||
|
|
||||||
@if (showLoginDialog) {
|
@if (showLoginDialog) {
|
||||||
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
|
<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 { LoginDialogComponent } from '../login-dialog/login-dialog.component';
|
||||||
import { RegisterDialogComponent } from '../register-dialog/register-dialog.component';
|
import { RegisterDialogComponent } from '../register-dialog/register-dialog.component';
|
||||||
import { ThemeService } from '../../services/theme.service';
|
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({
|
@Component({
|
||||||
selector: 'app-toolbar',
|
selector: 'app-toolbar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent],
|
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent, ChallengeNotificationComponent],
|
||||||
templateUrl: './toolbar.component.html',
|
templateUrl: './toolbar.component.html',
|
||||||
styleUrl: './toolbar.component.css'
|
styleUrl: './toolbar.component.css'
|
||||||
})
|
})
|
||||||
@@ -21,12 +24,16 @@ export class ToolbarComponent implements OnInit {
|
|||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
private readonly authDialogService = inject(AuthDialogService);
|
private readonly authDialogService = inject(AuthDialogService);
|
||||||
private readonly themeService = inject(ThemeService);
|
private readonly themeService = inject(ThemeService);
|
||||||
|
private readonly challengeEventService = inject(ChallengeEventService);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
currentUser: CurrentUser | null = null;
|
currentUser: CurrentUser | null = null;
|
||||||
showLoginDialog = false;
|
showLoginDialog = false;
|
||||||
showRegisterDialog = false;
|
showRegisterDialog = false;
|
||||||
isDarkMode = false;
|
isDarkMode = false;
|
||||||
|
incomingChallenges: Challenge[] = [];
|
||||||
|
showNotificationMenu = false;
|
||||||
|
displayedChallenge: Challenge | null = null;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.authService.currentUser$
|
this.authService.currentUser$
|
||||||
@@ -47,6 +54,22 @@ export class ToolbarComponent implements OnInit {
|
|||||||
.subscribe((isDarkMode) => {
|
.subscribe((isDarkMode) => {
|
||||||
this.isDarkMode = 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 {
|
openLoginDialog(): void {
|
||||||
@@ -84,4 +107,34 @@ export class ToolbarComponent implements OnInit {
|
|||||||
onRegisterSuccess(): void {
|
onRegisterSuccess(): void {
|
||||||
this.closeRegisterDialog();
|
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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,13 +46,6 @@
|
|||||||
<div class="w" *ngFor="let win of windows['wA3']" [ngStyle]="win.style"></div>
|
<div class="w" *ngFor="let win of windows['wA3']" [ngStyle]="win.style"></div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="bwrap" style="left:21%;width:15%;">
|
<div class="bwrap" style="left:21%;width:15%;">
|
||||||
@@ -101,8 +94,8 @@
|
|||||||
<div class="bb-tag">WELCOME</div>
|
<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-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>
|
<div class="bb-subtitle">Play your next move from the skyline.</div>
|
||||||
<button type="button" class="app-btn" (click)="startOneVsOne()" [disabled]="creating">
|
<button type="button" class="app-btn" (click)="openChallengeDialog()" [disabled]="creating">
|
||||||
{{ creating ? 'CREATING...' : 'START NOW →' }}
|
{{ creating ? 'CREATING...' : 'CREATE GAME →' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,6 +138,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Speech Bubble -->
|
||||||
|
@if (showSpeechBubble) {
|
||||||
|
<div class="speech-bubble-container" (click)="onSpeechBubbleClick()">
|
||||||
|
<div class="speech-bubble">
|
||||||
|
<div class="bubble-text">{{ bubbleMessage }}</div>
|
||||||
|
<div class="bubble-tail"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Speech Bubble -->
|
<!-- Speech Bubble -->
|
||||||
@if (showSpeechBubble) {
|
@if (showSpeechBubble) {
|
||||||
<div class="speech-bubble-container" (click)="onSpeechBubbleClick()">
|
<div class="speech-bubble-container" (click)="onSpeechBubbleClick()">
|
||||||
@@ -155,6 +156,39 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Zoomed Window View -->
|
||||||
|
@if (isZoomedIn) {
|
||||||
|
<div class="zoom-overlay" (click)="onZoomedViewClick()" (mousemove)="onMouseMove($event)" (mouseup)="onMouseUp()"
|
||||||
|
(mouseleave)="onMouseUp()">
|
||||||
|
<div class="zoom-window-wrapper" (click)="$event.stopPropagation()">
|
||||||
|
<div class="zoom-window-frame">
|
||||||
|
<div class="zoom-player-2">
|
||||||
|
<img src="/assets/arabian-chess/player-two.gif" alt="Player 2" class="player-2-gif"
|
||||||
|
(click)="$event.stopPropagation()" />
|
||||||
|
@if (showSecondSpeechBubble) {
|
||||||
|
<div class="second-speech-bubble">
|
||||||
|
<div class="bubble-text">Feed me! 🍖</div>
|
||||||
|
<div class="bubble-tail"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (showHappyBubble) {
|
||||||
|
<div class="happy-speech-bubble">
|
||||||
|
<div class="bubble-text">Happy meow! 😸</div>
|
||||||
|
<div class="bubble-tail"></div>
|
||||||
|
</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>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Zoomed Window View -->
|
<!-- Zoomed Window View -->
|
||||||
@if (isZoomedIn) {
|
@if (isZoomedIn) {
|
||||||
<div class="zoom-overlay" (click)="onZoomedViewClick()" (mousemove)="onMouseMove($event)" (mouseup)="onMouseUp()"
|
<div class="zoom-overlay" (click)="onZoomedViewClick()" (mousemove)="onMouseMove($event)" (mouseup)="onMouseUp()"
|
||||||
@@ -193,6 +227,20 @@
|
|||||||
<div class="ground"></div>
|
<div class="ground"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (showDifficultyDialog) {
|
||||||
|
<div class="dialog-overlay" (click)="closeDifficultyDialog()">
|
||||||
|
<div class="dialog-card" (click)="$event.stopPropagation()">
|
||||||
|
<div class="dialog-title">SELECT DIFFICULTY</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button type="button" class="app-btn" (click)="startVsBot('easy')" [disabled]="creating">EASY</button>
|
||||||
|
<button type="button" class="app-btn" (click)="startVsBot('medium')" [disabled]="creating">MEDIUM</button>
|
||||||
|
<button type="button" class="app-btn" (click)="startVsBot('hard')" [disabled]="creating">HARD</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="haze"></div>
|
||||||
|
<div class="ground"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (showDifficultyDialog) {
|
@if (showDifficultyDialog) {
|
||||||
<div class="dialog-overlay" (click)="closeDifficultyDialog()">
|
<div class="dialog-overlay" (click)="closeDifficultyDialog()">
|
||||||
<div class="dialog-card" (click)="$event.stopPropagation()">
|
<div class="dialog-card" (click)="$event.stopPropagation()">
|
||||||
@@ -211,6 +259,7 @@
|
|||||||
<div class="dialog-card" (click)="$event.stopPropagation()">
|
<div class="dialog-card" (click)="$event.stopPropagation()">
|
||||||
<div class="dialog-title">MORE OPTIONS</div>
|
<div class="dialog-title">MORE OPTIONS</div>
|
||||||
<div class="dialog-actions">
|
<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>
|
<button type="button" class="app-btn" (click)="openImportDialog()">IMPORT GAME</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -264,13 +313,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (showChallengeDialog) {
|
@if (showChallengeDialog) {
|
||||||
<div class="dialog-overlay" (click)="closeChallengeDialog()">
|
<app-challenge-create-dialog (closeChallengeDialog)="closeChallengeDialog()"></app-challenge-create-dialog>
|
||||||
<div class="dialog-card" (click)="$event.stopPropagation()">
|
|
||||||
<app-challenge-create-dialog (closeChallengeDialog)="closeChallengeDialog()"></app-challenge-create-dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (errorMessage) {
|
||||||
|
<p class="error-banner">{{ errorMessage }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@if (errorMessage) {
|
@if (errorMessage) {
|
||||||
<p class="error-banner">{{ errorMessage }}</p>
|
<p class="error-banner">{{ errorMessage }}</p>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core';
|
import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core';
|
||||||
|
import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { finalize } from 'rxjs';
|
import { finalize } from 'rxjs';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { getErrorMessage } from '../../core/http/error-message.util';
|
import { getErrorMessage } from '../../core/http/error-message.util';
|
||||||
import { CurrentUser } from '../../models/auth.models';
|
import { CurrentUser } from '../../models/auth.models';
|
||||||
import { AuthDialogService } from '../../services/auth-dialog.service';
|
import { AuthDialogService } from '../../services/auth-dialog.service';
|
||||||
import { AuthService } from '../../services/auth.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 { GameApiService } from '../../services/game-api.service';
|
||||||
import { ThemeService } from '../../services/theme.service';
|
import { ThemeService } from '../../services/theme.service';
|
||||||
import { ChallengeCreateDialogComponent } from '../../components/challenge-create-dialog/challenge-create-dialog.component';
|
import { ChallengeCreateDialogComponent } from '../../components/challenge-create-dialog/challenge-create-dialog.component';
|
||||||
@@ -30,28 +35,20 @@ interface WindowCell {
|
|||||||
style: Record<string, string>;
|
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({
|
@Component({
|
||||||
selector: 'app-welcome',
|
selector: 'app-welcome',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, ChallengeCreateDialogComponent],
|
imports: [CommonModule, FormsModule, ChallengeCreateDialogComponent],
|
||||||
templateUrl: './welcome.component.html',
|
templateUrl: './welcome.component.html',
|
||||||
styleUrls: ['./welcome.component.css']
|
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 {
|
export class WelcomeComponent implements OnInit, OnDestroy {
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
@@ -61,13 +58,19 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
creating = false;
|
creating = false;
|
||||||
joiningGame = false;
|
joiningGame = false;
|
||||||
importing = false;
|
importing = false;
|
||||||
|
showChallengeDialog = false;
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
|
|
||||||
showDifficultyDialog = false;
|
showDifficultyDialog = false;
|
||||||
showOptionsDialog = false;
|
showOptionsDialog = false;
|
||||||
showJoinDialog = false;
|
showJoinDialog = false;
|
||||||
showImportDialog = false;
|
showImportDialog = false;
|
||||||
showChallengeDialog = false;
|
|
||||||
|
|
||||||
|
showDifficultyDialog = false;
|
||||||
|
showOptionsDialog = false;
|
||||||
|
showJoinDialog = false;
|
||||||
|
showImportDialog = false;
|
||||||
|
|
||||||
gameIdInput = '';
|
gameIdInput = '';
|
||||||
importMode: ImportMode = 'fen';
|
importMode: ImportMode = 'fen';
|
||||||
@@ -105,6 +108,43 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
private coolColors = ['#7de8ff', '#00d5ff', '#5bc0de', '#31b0d5', '#4fc3f7', '#29b6f6'];
|
private coolColors = ['#7de8ff', '#00d5ff', '#5bc0de', '#31b0d5', '#4fc3f7', '#29b6f6'];
|
||||||
private coolGlowColors = ['#00d5ff', '#00d5ff', '#31b0d5', '#31b0d5', '#03a9f4', '#0288d1'];
|
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<string, WindowCell[]> = {};
|
||||||
|
|
||||||
|
private flickerIntervalId: ReturnType<typeof setInterval> | undefined;
|
||||||
|
private speechBubbleTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
private zoomTimeoutId: ReturnType<typeof setTimeout> | 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 warmColors = ['#ffe88a', '#ffcc30', '#f0ad4e', '#ec971f', '#ffb74d', '#ffa726'];
|
||||||
private warmGlowColors = ['#ffcc30', '#ffcc30', '#ec971f', '#ec971f', '#ff9800', '#fb8c00'];
|
private warmGlowColors = ['#ffcc30', '#ffcc30', '#ec971f', '#ec971f', '#ff9800', '#fb8c00'];
|
||||||
|
|
||||||
@@ -171,6 +211,20 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
this.errorMessage = '';
|
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 {
|
openOptionsDialog(): void {
|
||||||
this.closeAllDialogs();
|
this.closeAllDialogs();
|
||||||
this.showOptionsDialog = true;
|
this.showOptionsDialog = true;
|
||||||
@@ -224,21 +278,11 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startOneVsOne(): void {
|
startOneVsOne(): void {
|
||||||
if (!this.requireAuth(() => this.openChallengeDialog())) {
|
if (!this.requireAuth(() => this.performStartOneVsOne())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openChallengeDialog();
|
this.performStartOneVsOne();
|
||||||
}
|
|
||||||
|
|
||||||
openChallengeDialog(): void {
|
|
||||||
this.closeAllDialogs();
|
|
||||||
this.showChallengeDialog = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeChallengeDialog(): void {
|
|
||||||
this.showChallengeDialog = false;
|
|
||||||
this.errorMessage = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
startVsBot(difficulty: Difficulty): void {
|
startVsBot(difficulty: Difficulty): void {
|
||||||
@@ -363,7 +407,163 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
action();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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' }
|
||||||
|
});
|
||||||
|
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 {
|
||||||
private performStartVsBot(difficulty: Difficulty): void {
|
private performStartVsBot(difficulty: Difficulty): void {
|
||||||
if (this.creating) {
|
if (this.creating) {
|
||||||
return;
|
return;
|
||||||
@@ -372,6 +572,7 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
this.creating = true;
|
this.creating = true;
|
||||||
this.showDifficultyDialog = false;
|
this.showDifficultyDialog = false;
|
||||||
|
this.showDifficultyDialog = false;
|
||||||
|
|
||||||
this.gameApi
|
this.gameApi
|
||||||
.createGameVsBot(difficulty)
|
.createGameVsBot(difficulty)
|
||||||
@@ -381,6 +582,9 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
void this.router.navigate(['/game', game.gameId], {
|
void this.router.navigate(['/game', game.gameId], {
|
||||||
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
|
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
|
||||||
});
|
});
|
||||||
|
void this.router.navigate(['/game', game.gameId], {
|
||||||
|
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.errorMessage = getErrorMessage(error, 'Unable to create a game against bot.');
|
this.errorMessage = getErrorMessage(error, 'Unable to create a game against bot.');
|
||||||
@@ -388,6 +592,9 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private performSubmitJoinGame(): void {
|
||||||
|
const gameId = this.gameIdInput.trim();
|
||||||
|
if (this.joiningGame || !gameId) {
|
||||||
private performSubmitJoinGame(): void {
|
private performSubmitJoinGame(): void {
|
||||||
const gameId = this.gameIdInput.trim();
|
const gameId = this.gameIdInput.trim();
|
||||||
if (this.joiningGame || !gameId) {
|
if (this.joiningGame || !gameId) {
|
||||||
@@ -398,6 +605,7 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
this.joiningGame = true;
|
this.joiningGame = true;
|
||||||
|
|
||||||
this.gameApi
|
this.gameApi
|
||||||
|
.getGame(gameId)
|
||||||
.getGame(gameId)
|
.getGame(gameId)
|
||||||
.pipe(finalize(() => (this.joiningGame = false)))
|
.pipe(finalize(() => (this.joiningGame = false)))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
@@ -406,6 +614,10 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
void this.router.navigate(['/game', game.gameId], {
|
void this.router.navigate(['/game', game.gameId], {
|
||||||
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
|
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
|
||||||
});
|
});
|
||||||
|
this.closeJoinDialog();
|
||||||
|
void this.router.navigate(['/game', game.gameId], {
|
||||||
|
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.errorMessage = getErrorMessage(error, 'Unable to find or join the game.');
|
this.errorMessage = getErrorMessage(error, 'Unable to find or join the game.');
|
||||||
@@ -444,6 +656,7 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
this.showOptionsDialog = false;
|
this.showOptionsDialog = false;
|
||||||
this.showJoinDialog = false;
|
this.showJoinDialog = false;
|
||||||
this.showImportDialog = false;
|
this.showImportDialog = false;
|
||||||
|
this.showChallengeDialog = false;
|
||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,6 +771,120 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
}, 2800);
|
}, 2800);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
private stopWindowFlicker(): void {
|
||||||
if (this.flickerIntervalId === undefined) {
|
if (this.flickerIntervalId === undefined) {
|
||||||
return;
|
return;
|
||||||
@@ -566,6 +893,45 @@ export class WelcomeComponent implements OnInit, OnDestroy {
|
|||||||
this.flickerIntervalId = undefined;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
private randomFlicker(): void {
|
private randomFlicker(): void {
|
||||||
const allWindows = Object.values(this.windows).flat();
|
const allWindows = Object.values(this.windows).flat();
|
||||||
if (allWindows.length === 0) {
|
if (allWindows.length === 0) {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { HttpInterceptorFn } from '@angular/common/http';
|
|||||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// Add token to protected endpoints only (not registration or login)
|
||||||
const isProtectedEndpoint =
|
const isProtectedEndpoint =
|
||||||
req.url.includes('/api/account/me') ||
|
req.url.includes('/api/account/me') ||
|
||||||
req.url.includes('/api/account/bots') ||
|
req.url.includes('/api/account/bots') ||
|
||||||
req.url.includes('/api/account/official-bots') ||
|
req.url.includes('/api/account/official-bots') ||
|
||||||
req.url.includes('/api/board/game') ||
|
|
||||||
req.url.includes('/api/challenge');
|
req.url.includes('/api/challenge');
|
||||||
|
|
||||||
if (token && isProtectedEndpoint) {
|
if (token && isProtectedEndpoint) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export class GameStreamService {
|
|||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private streamSubscription: Subscription | null = null;
|
private streamSubscription: Subscription | null = null;
|
||||||
private pollSubscription: Subscription | null = null;
|
private pollSubscription: Subscription | null = null;
|
||||||
private lastGameStateHash: string | null = null;
|
|
||||||
|
|
||||||
startStreaming(
|
startStreaming(
|
||||||
gameId: string,
|
gameId: string,
|
||||||
@@ -21,10 +20,7 @@ export class GameStreamService {
|
|||||||
.streamGame(gameId)
|
.streamGame(gameId)
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (event) => {
|
next: (event) => onEvent(event),
|
||||||
this.lastGameStateHash = JSON.stringify(event);
|
|
||||||
onEvent(event);
|
|
||||||
},
|
|
||||||
error: () => {
|
error: () => {
|
||||||
onStreamError();
|
onStreamError();
|
||||||
this.startPolling(gameId, onEvent);
|
this.startPolling(gameId, onEvent);
|
||||||
@@ -41,7 +37,7 @@ export class GameStreamService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pollSubscription = interval(5000)
|
this.pollSubscription = interval(1500)
|
||||||
.pipe(
|
.pipe(
|
||||||
startWith(0),
|
startWith(0),
|
||||||
switchMap(() => this.gameApi.getGame(gameId)),
|
switchMap(() => this.gameApi.getGame(gameId)),
|
||||||
@@ -49,16 +45,11 @@ export class GameStreamService {
|
|||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (game) => {
|
next: (game) => {
|
||||||
// Only emit if game state changed to avoid unnecessary updates
|
const event: GameStreamEvent = {
|
||||||
const stateHash = JSON.stringify(game.state);
|
type: 'gameFull',
|
||||||
if (this.lastGameStateHash !== stateHash) {
|
game
|
||||||
this.lastGameStateHash = stateHash;
|
};
|
||||||
const event: GameStreamEvent = {
|
onEvent(event);
|
||||||
type: 'gameFull',
|
|
||||||
game
|
|
||||||
};
|
|
||||||
onEvent(event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,6 +59,5 @@ export class GameStreamService {
|
|||||||
this.pollSubscription?.unsubscribe();
|
this.pollSubscription?.unsubscribe();
|
||||||
this.streamSubscription = null;
|
this.streamSubscription = null;
|
||||||
this.pollSubscription = null;
|
this.pollSubscription = null;
|
||||||
this.lastGameStateHash = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = () => {
|
ws.onopen = () => {
|
||||||
connected = true;
|
connected = true;
|
||||||
clearTimeout(connectionTimeoutId);
|
|
||||||
console.log(`[StreamHandler] WebSocket connected for ${gameId}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (message) => {
|
ws.onmessage = (message) => {
|
||||||
@@ -108,23 +97,19 @@ export class StreamHandlerService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.warn(`[StreamHandler] WebSocket error for ${gameId}:`, error);
|
console.warn(`[StreamHandler] WebSocket error for ${gameId}, attempting NDJSON fallback:`, error);
|
||||||
clearTimeout(connectionTimeoutId);
|
if (!connected) {
|
||||||
if (!connected && !fallbackActive) {
|
|
||||||
void startNdjsonFallback();
|
void startNdjsonFallback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
clearTimeout(connectionTimeoutId);
|
|
||||||
console.warn(`[StreamHandler] WebSocket closed for ${gameId}, connected=${connected}`);
|
console.warn(`[StreamHandler] WebSocket closed for ${gameId}, connected=${connected}`);
|
||||||
if (connected) {
|
if (!connected) {
|
||||||
// Connection was established but closed, stream is complete
|
|
||||||
observer.complete();
|
|
||||||
} else if (!fallbackActive) {
|
|
||||||
// Connection never established, try fallback
|
|
||||||
console.log(`[StreamHandler] Starting NDJSON fallback for ${gameId}`);
|
console.log(`[StreamHandler] Starting NDJSON fallback for ${gameId}`);
|
||||||
void startNdjsonFallback();
|
void startNdjsonFallback();
|
||||||
|
} else {
|
||||||
|
observer.complete();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=2
|
MINOR=1
|
||||||
PATCH=2
|
PATCH=0
|
||||||
|
|||||||
Reference in New Issue
Block a user