feat: login and register, style is not ready

This commit is contained in:
Lala, Shahd
2026-05-03 20:44:01 +00:00
parent 3b757d7ff7
commit aa70083aed
27 changed files with 1083 additions and 308 deletions
+171 -105
View File
@@ -1,10 +1,15 @@
import { CommonModule } from '@angular/common';
import { Component, OnDestroy, OnInit } 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 { 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 { GameApiService } from '../../services/game-api.service';
import { ThemeService } from '../../services/theme.service';
type Difficulty = 'easy' | 'medium' | 'hard';
type ImportMode = 'fen' | 'pgn';
@@ -32,6 +37,11 @@ interface WindowCell {
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);
creating = false;
joiningGame = false;
importing = false;
@@ -48,6 +58,9 @@ export class WelcomeComponent implements OnInit, OnDestroy {
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;
@@ -82,10 +95,30 @@ export class WelcomeComponent implements OnInit, OnDestroy {
private readonly router: Router,
private readonly gameApi: GameApiService
) {
this.initTheme();
}
ngOnInit(): void {
this.themeService.darkMode$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((isDarkMode) => {
this.isSunsetMode = !isDarkMode;
this.modeBadge = this.isSunsetMode ? 'SUNSET MODE' : 'NIGHT MODE';
});
this.authService.currentUser$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.currentUser = user;
this.maybeRunPendingAction();
});
this.authDialogService.dialogState$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
this.authDialogState = state;
this.maybeRunPendingAction();
});
this.generateStars(220);
this.generateBackgroundBuildings();
this.generateWindowsForAllBuildings();
@@ -107,21 +140,11 @@ export class WelcomeComponent implements OnInit, OnDestroy {
}
}
toggleTheme(): void {
this.isSunsetMode = !this.isSunsetMode;
this.modeBadge = this.isSunsetMode ? 'SUNSET MODE' : 'NIGHT MODE';
if (!this.isSunsetMode) {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
openDifficultyDialog(): void {
if (!this.requireAuth(() => this.showDifficultyDialog = true)) {
return;
}
document.documentElement.removeAttribute('data-theme');
localStorage.removeItem('theme');
}
openDifficultyDialog(): void {
this.closeAllDialogs();
this.showDifficultyDialog = true;
}
@@ -142,6 +165,10 @@ export class WelcomeComponent implements OnInit, OnDestroy {
}
openJoinDialog(): void {
if (!this.requireAuth(() => this.showJoinDialog = true)) {
return;
}
this.closeAllDialogs();
this.showJoinDialog = true;
}
@@ -156,6 +183,10 @@ export class WelcomeComponent implements OnInit, OnDestroy {
}
openImportDialog(): void {
if (!this.requireAuth(() => this.showImportDialog = true)) {
return;
}
this.closeAllDialogs();
this.showImportDialog = true;
}
@@ -176,101 +207,35 @@ export class WelcomeComponent implements OnInit, OnDestroy {
}
startOneVsOne(): void {
if (this.creating) {
if (!this.requireAuth(() => this.performStartOneVsOne())) {
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.');
}
});
this.performStartOneVsOne();
}
startVsBot(difficulty: Difficulty): void {
if (this.creating) {
if (!this.requireAuth(() => this.performStartVsBot(difficulty))) {
return;
}
this.errorMessage = '';
this.creating = true;
this.showDifficultyDialog = false;
this.gameApi
.createGameVsBot(difficulty)
.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 against bot.');
}
});
this.performStartVsBot(difficulty);
}
submitJoinGame(): void {
const gameId = this.gameIdInput.trim();
if (this.joiningGame || !gameId) {
if (!this.requireAuth(() => this.performSubmitJoinGame())) {
return;
}
this.errorMessage = '';
this.joiningGame = true;
this.gameApi
.getGame(gameId)
.pipe(finalize(() => (this.joiningGame = false)))
.subscribe({
next: (game) => {
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.');
}
});
this.performSubmitJoinGame();
}
submitImportGame(): void {
const trimmedImport = this.importText.trim();
if (this.importing || !trimmedImport) {
if (!this.requireAuth(() => this.performSubmitImportGame())) {
return;
}
this.errorMessage = '';
this.importing = true;
const importRequest =
this.importMode === 'fen' ? this.gameApi.importFen(trimmedImport) : this.gameApi.importPgn(trimmedImport);
importRequest.pipe(finalize(() => (this.importing = false))).subscribe({
next: (game) => {
this.closeImportDialog();
void this.router.navigate(['/game', game.gameId], {
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
});
},
error: (error) => {
const defaultMessage = this.importMode === 'fen' ? 'Unable to import FEN.' : 'Unable to import PGN.';
this.errorMessage = getErrorMessage(error, defaultMessage);
}
});
this.performSubmitImportGame();
}
onSpeechBubbleClick(): void {
@@ -313,7 +278,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.meatX = event.clientX - this.meatDragOffsetX;
this.meatY = event.clientY - this.meatDragOffsetY;
// Get gif element position
const gifElement = document.querySelector('.player-2-gif') as HTMLElement;
if (!gifElement) {
return;
@@ -323,7 +287,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
const gifCenterX = gifRect.left + gifRect.width / 2;
const gifCenterY = gifRect.top + gifRect.height / 2;
// Get meat center position
const meatElement = document.querySelector('.meat-emoji') as HTMLElement;
if (!meatElement) {
return;
@@ -333,12 +296,10 @@ export class WelcomeComponent implements OnInit, OnDestroy {
const meatCenterX = meatRect.left + meatRect.width / 2;
const meatCenterY = meatRect.top + meatRect.height / 2;
// Calculate distance
const distance = Math.sqrt(
Math.pow(meatCenterX - gifCenterX, 2) + Math.pow(meatCenterY - gifCenterY, 2)
);
// If meat is close enough to gif center (within 50px), trigger the interaction
if (distance < 50) {
this.onMeatFed();
}
@@ -355,6 +316,124 @@ export class WelcomeComponent implements OnInit, OnDestroy {
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' }
});
},
error: (error) => {
this.errorMessage = getErrorMessage(error, 'Unable to create a game.');
}
});
}
private performStartVsBot(difficulty: Difficulty): void {
if (this.creating) {
return;
}
this.errorMessage = '';
this.creating = true;
this.showDifficultyDialog = false;
this.gameApi
.createGameVsBot(difficulty)
.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 against bot.');
}
});
}
private performSubmitJoinGame(): void {
const gameId = this.gameIdInput.trim();
if (this.joiningGame || !gameId) {
return;
}
this.errorMessage = '';
this.joiningGame = true;
this.gameApi
.getGame(gameId)
.pipe(finalize(() => (this.joiningGame = false)))
.subscribe({
next: (game) => {
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.');
}
});
}
private performSubmitImportGame(): void {
const trimmedImport = this.importText.trim();
if (this.importing || !trimmedImport) {
return;
}
this.errorMessage = '';
this.importing = true;
const importRequest =
this.importMode === 'fen' ? this.gameApi.importFen(trimmedImport) : this.gameApi.importPgn(trimmedImport);
importRequest.pipe(finalize(() => (this.importing = false))).subscribe({
next: (game) => {
this.closeImportDialog();
void this.router.navigate(['/game', game.gameId], {
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
});
},
error: (error) => {
const defaultMessage = this.importMode === 'fen' ? 'Unable to import FEN.' : 'Unable to import PGN.';
this.errorMessage = getErrorMessage(error, defaultMessage);
}
});
}
private closeAllDialogs(): void {
this.showDifficultyDialog = false;
this.showOptionsDialog = false;
@@ -363,19 +442,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.errorMessage = '';
}
private initTheme(): void {
const savedTheme = localStorage.getItem('theme');
this.isSunsetMode = savedTheme !== 'dark';
this.modeBadge = this.isSunsetMode ? 'SUNSET MODE' : 'NIGHT MODE';
if (!this.isSunsetMode) {
document.documentElement.setAttribute('data-theme', 'dark');
return;
}
document.documentElement.removeAttribute('data-theme');
}
private generateStars(count: number): void {
this.stars = Array.from({ length: count }, () => {
const size = Math.random() * 2 + 0.5;