feat: NCS-63 User account implementation (#2)

User Profile info, no game before login/register, menu bar

---------

Co-authored-by: Lala, Shahd <Shahd.Lala@sybit.de>
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-05-06 10:51:30 +02:00
parent 2de003e497
commit ff75c8ce2f
104 changed files with 4232 additions and 978 deletions
@@ -27,12 +27,56 @@
cursor: pointer;
}
.square[draggable='true'] {
cursor: grab;
}
.square.drag-source {
opacity: 0.65;
cursor: grabbing;
}
.square.drag-over {
outline: 3px dashed var(--color-primary);
outline-offset: -4px;
}
.square.light {
background-image: url('/arabian-chess/sprites/board/board_square_white.png');
background-image: url('/assets/arabian-chess/sprites/board/board_square_white.png');
}
.square.dark {
background-image: url('/arabian-chess/sprites/board/board_square_black.png');
background-image: url('/assets/arabian-chess/sprites/board/board_square_black.png');
}
.board-grid--classic {
border-radius: var(--border-radius-md);
}
.board-grid--classic .square {
background-image: none;
transition: filter 160ms ease;
}
.board-grid--classic .square.light {
background-color: #f3c8a0;
}
.board-grid--classic .square.dark {
background-color: #ba6d4b;
}
.board-grid--classic .square.drag-over {
outline-color: #5a2c28;
}
.board-grid--classic .square.selected {
outline-color: #5a2c28;
}
.board-grid--classic .square.highlighted::after {
background: #b9dad1;
border-color: #5a2c28;
}
.square.highlighted::after {
@@ -1,5 +1,5 @@
<div class="board-shell">
<div class="board-grid">
<div class="board-grid" [class.board-grid--classic]="boardTheme === 'classic'" [class.board-grid--arabian]="boardTheme === 'arabian'">
@for (square of squares; track trackByCoordinate($index, square)) {
<button
type="button"
@@ -8,13 +8,25 @@
[class.dark]="!square.isLight"
[class.selected]="isSelected(square)"
[class.highlighted]="isHighlighted(square)"
[class.drag-source]="isDraggingSource(square)"
[class.drag-over]="isDragOver(square)"
[attr.data-square]="square.coordinate"
(click)="onSquareClick(square)"
(dragover)="onSquareDragOver($event, square)"
(drop)="onSquareDrop($event, square)"
>
<app-chess-piece [pieceCode]="square.pieceCode" />
<app-chess-piece
[pieceCode]="square.pieceCode"
[boardTheme]="boardTheme"
[draggable]="!!square.pieceCode"
(pieceDragStart)="onPieceDragStart($event, square)"
(pieceDragEnd)="onSquareDragEnd()"
/>
</button>
}
</div>
<img class="board-bottom" src="/arabian-chess/sprites/board/board_bottom.png" alt="Board frame" />
@if (boardTheme === 'arabian') {
<img class="board-bottom" src="/assets/arabian-chess/sprites/board/board_bottom.png" alt="Board frame" />
}
</div>
@@ -7,6 +7,8 @@ interface BoardSquare {
pieceCode: string | null;
}
type BoardTheme = 'arabian' | 'classic';
@Component({
selector: 'app-chess-board',
standalone: true,
@@ -18,10 +20,14 @@ export class ChessBoardComponent implements OnChanges {
@Input({ required: true }) fen = '';
@Input() selectedSquare: string | null = null;
@Input() highlightedSquares: string[] = [];
@Input() boardTheme: BoardTheme = 'arabian';
@Output() squareSelected = new EventEmitter<string>();
squares: BoardSquare[] = [];
private highlightedSquareSet = new Set<string>();
private draggingFromSquare: string | null = null;
private dragOverSquare: string | null = null;
private suppressNextClick = false;
ngOnChanges(changes: SimpleChanges): void {
if (changes['fen']) {
@@ -38,9 +44,61 @@ export class ChessBoardComponent implements OnChanges {
}
onSquareClick(square: BoardSquare): void {
if (this.suppressNextClick) {
this.suppressNextClick = false;
return;
}
this.squareSelected.emit(square.coordinate);
}
onPieceDragStart(event: DragEvent, square: BoardSquare): void {
if (!square.pieceCode) {
event.preventDefault();
return;
}
this.draggingFromSquare = square.coordinate;
if (event.dataTransfer) {
event.dataTransfer.setData('text/plain', square.coordinate);
event.dataTransfer.effectAllowed = 'move';
}
this.squareSelected.emit(square.coordinate);
}
onSquareDragOver(event: DragEvent, square: BoardSquare): void {
if (!this.draggingFromSquare) {
return;
}
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
this.dragOverSquare = square.coordinate === this.draggingFromSquare ? null : square.coordinate;
}
onSquareDrop(event: DragEvent, square: BoardSquare): void {
event.preventDefault();
if (!this.draggingFromSquare) {
return;
}
const fromSquare = this.draggingFromSquare;
this.clearDragState();
if (fromSquare === square.coordinate) {
return;
}
this.suppressNextClick = true;
this.squareSelected.emit(square.coordinate);
}
onSquareDragEnd(): void {
this.clearDragState();
}
isSelected(square: BoardSquare): boolean {
return this.selectedSquare === square.coordinate;
}
@@ -49,6 +107,14 @@ export class ChessBoardComponent implements OnChanges {
return this.highlightedSquareSet.has(square.coordinate);
}
isDraggingSource(square: BoardSquare): boolean {
return this.draggingFromSquare === square.coordinate;
}
isDragOver(square: BoardSquare): boolean {
return this.dragOverSquare === square.coordinate;
}
private buildSquares(fen: string): BoardSquare[] {
const placement = fen.split(' ')[0] ?? '';
const rows = placement.split('/');
@@ -87,4 +153,9 @@ export class ChessBoardComponent implements OnChanges {
pieceCode
};
}
private clearDragState(): void {
this.draggingFromSquare = null;
this.dragOverSquare = null;
}
}
@@ -3,7 +3,15 @@
height: clamp(40px, 8cqh, 120px);
display: block;
object-fit: contain;
pointer-events: none;
pointer-events: auto;
}
.piece[draggable='true'] {
cursor: grab;
}
.piece[draggable='true']:active {
cursor: grabbing;
}
@media (max-width: 991px) {
@@ -1,3 +1,10 @@
@if (pieceCode) {
<img class="piece" [src]="spriteUrl" [alt]="pieceCode" />
<img
class="piece"
[src]="spriteUrl"
[alt]="pieceCode"
[attr.draggable]="draggable ? 'true' : null"
(dragstart)="onDragStart($event)"
(dragend)="onDragEnd()"
/>
}
@@ -1,4 +1,6 @@
import { Component, Input } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
type BoardTheme = 'arabian' | 'classic';
@Component({
selector: 'app-chess-piece',
@@ -8,18 +10,44 @@ import { Component, Input } from '@angular/core';
})
export class ChessPieceComponent {
@Input({ required: true }) pieceCode: string | null = null;
@Input() boardTheme: BoardTheme = 'arabian';
@Input() draggable = false;
@Output() pieceDragStart = new EventEmitter<DragEvent>();
@Output() pieceDragEnd = new EventEmitter<void>();
onDragStart(event: DragEvent): void {
if (!this.draggable) {
event.preventDefault();
return;
}
this.pieceDragStart.emit(event);
}
onDragEnd(): void {
this.pieceDragEnd.emit();
}
get spriteUrl(): string {
if (!this.pieceCode) {
return '';
}
const color = this.pieceCode === this.pieceCode.toUpperCase() ? 'white' : 'black';
const pieceName = this.getPieceName(this.pieceCode.toLowerCase());
return `/arabian-chess/sprites/pieces/${color}_${pieceName}.png`;
const isWhite = this.pieceCode === this.pieceCode.toUpperCase();
const pieceCode = this.pieceCode.toLowerCase();
if (this.boardTheme === 'classic') {
const colorPrefix = isWhite ? 'w' : 'b';
const classicPieceName = this.getClassicPieceName(pieceCode);
return `/assets/ChessAssets/${colorPrefix}_${classicPieceName}.png`;
}
const arabianColor = isWhite ? 'white' : 'black';
const arabianPieceName = this.getArabianPieceName(pieceCode);
return `/assets/arabian-chess/sprites/pieces/${arabianColor}_${arabianPieceName}.png`;
}
private getPieceName(piece: string): string {
private getArabianPieceName(piece: string): string {
switch (piece) {
case 'k':
return 'king';
@@ -37,4 +65,23 @@ export class ChessPieceComponent {
return 'pawn';
}
}
private getClassicPieceName(piece: string): string {
switch (piece) {
case 'k':
return 'King';
case 'q':
return 'Queen';
case 'r':
return 'Rook';
case 'b':
return 'Bishop';
case 'n':
return 'Knight';
case 'p':
return 'Pawn';
default:
return 'Pawn';
}
}
}
@@ -1,3 +1,5 @@
@import '../../button-template.css';
.input-card {
background: var(--color-bg-card);
border: var(--border-width) solid var(--color-border);
@@ -40,22 +42,6 @@
}
.input-card .btn {
border: var(--button-border);
border-radius: var(--border-radius-sm);
background: var(--color-bg-button);
color: var(--color-text-primary);
padding: var(--button-padding);
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s, color 0.2s;
}
.input-card .btn:hover {
background: var(--color-bg-button-hover);
color: var(--color-text-button-hover);
}
.hint-text {
margin: 0;
color: var(--color-text-primary);
@@ -2,30 +2,18 @@
<label>{{ label }}</label>
@if (inputType === 'textarea') {
<textarea
#textareaInput
[placeholder]="placeholder"
[value]="value"
[rows]="rows"
(input)="onValueChange(textareaInput.value)"
class="form-input"
></textarea>
<textarea #textareaInput [placeholder]="placeholder" [value]="value" [rows]="rows"
(input)="onValueChange(textareaInput.value)" class="form-input"></textarea>
} @else {
<input
#textInput
type="text"
[placeholder]="placeholder"
[value]="value"
(input)="onValueChange(textInput.value)"
class="form-input"
/>
<input #textInput type="text" [placeholder]="placeholder" [value]="value" (input)="onValueChange(textInput.value)"
class="form-input" />
}
<button type="button" class="btn w-100" (click)="onButtonClick()">
<button type="button" class="app-btn w-100" (click)="onButtonClick()">
{{ buttonLabel }}
</button>
@if (hintText) {
<p class="hint-text">{{ hintText }}</p>
<p class="hint-text">{{ hintText }}</p>
}
</section>
</section>
@@ -0,0 +1,68 @@
@import '../../button-template.css';
.dialog-overlay {
position: fixed;
inset: 0;
background: rgba(2, 2, 10, 0.58);
display: grid;
place-items: center;
z-index: 350;
padding: 1rem;
}
.dialog-card {
width: min(460px, 100%);
background: var(--dlg-bg);
border: 1.5px solid var(--dlg-border);
box-shadow: var(--bb-glow);
border-radius: 4px;
padding: 1rem;
display: grid;
gap: 0.7rem;
}
.dialog-title {
font-family: 'Bebas Neue', sans-serif;
font-size: 22px;
letter-spacing: 2px;
color: var(--bb-title);
text-align: center;
}
.dialog-input {
width: 100%;
background: rgba(4, 4, 20, 0.62);
border: 1px solid var(--bb-border);
color: var(--bb-title);
border-radius: 2px;
padding: 0.6rem 0.7rem;
font-family: 'Space Mono', monospace;
font-size: 13px;
}
.dialog-input:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(0, 213, 255, 0.25);
}
.dialog-textarea {
min-height: 120px;
resize: vertical;
}
.text-danger {
color: #dc3545;
font-size: 0.875rem;
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
@@ -0,0 +1,36 @@
<div class="dialog-overlay" (click)="closeDialog()">
<div class="dialog-card" (click)="$event.stopPropagation()">
<div class="dialog-title">LOGIN</div>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<label for="username" class="sr-only">Username</label>
<input id="username" type="text" class="dialog-input" formControlName="username" placeholder="Username"
[disabled]="isLoading" />
@if (loginForm.get('username')?.invalid && loginForm.get('username')?.touched) {
<small class="text-danger">Username must be at least 3 characters</small>
}
<label for="password" class="sr-only">Password</label>
<input id="password" type="password" class="dialog-input" formControlName="password" placeholder="Password"
[disabled]="isLoading" />
@if (loginForm.get('password')?.invalid && loginForm.get('password')?.touched) {
<small class="text-danger">Password must be at least 6 characters</small>
}
@if (errorMessage) {
<div class="error-banner">{{ errorMessage }}</div>
}
<div class="dialog-actions">
<button type="button" class="app-btn" (click)="openRegister()">Create account</button>
<button type="button" class="app-btn" (click)="closeDialog()">Cancel</button>
<button type="submit" class="app-btn" [disabled]="isLoading || loginForm.invalid">
@if (isLoading) {
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
}
Login
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,62 @@
import { Component, EventEmitter, Output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service';
import { AuthDialogService } from '../../services/auth-dialog.service';
@Component({
selector: 'app-login-dialog',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './login-dialog.component.html',
styleUrl: './login-dialog.component.css'
})
export class LoginDialogComponent {
@Output() onClose = new EventEmitter<void>();
@Output() onSuccess = new EventEmitter<void>();
private readonly authService = inject(AuthService);
private readonly authDialogService = inject(AuthDialogService);
private readonly formBuilder = inject(FormBuilder);
loginForm: FormGroup;
errorMessage: string | null = null;
isLoading = false;
constructor() {
this.loginForm = this.formBuilder.group({
username: ['', [Validators.required, Validators.minLength(3)]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
onSubmit(): void {
if (this.loginForm.invalid) {
this.errorMessage = 'Please fill in all fields correctly';
return;
}
this.isLoading = true;
this.errorMessage = null;
const { username, password } = this.loginForm.value;
this.authService.login(username, password).subscribe({
next: () => {
this.isLoading = false;
this.onSuccess.emit();
},
error: (err) => {
this.isLoading = false;
this.errorMessage = err.error?.message || 'Login failed. Please try again.';
}
});
}
closeDialog(): void {
this.onClose.emit();
}
openRegister(): void {
this.authDialogService.openRegister();
}
}
@@ -1,3 +1,5 @@
@import '../../button-template.css';
.promotion-dialog-overlay {
position: fixed;
top: 0;
@@ -20,9 +22,10 @@
}
.promotion-dialog {
background: white;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
background: var(--dlg-bg, white);
border: 1.5px solid var(--dlg-border, #ddd);
border-radius: 4px;
box-shadow: var(--bb-glow, 0 4px 16px rgba(0, 0, 0, 0.2));
max-width: 400px;
width: 90%;
animation: slideUp 0.3s ease;
@@ -44,32 +47,14 @@
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e0e0e0;
border-bottom: 1px solid var(--bb-border, #e0e0e0);
h3 {
margin: 0;
font-family: 'Bebas Neue', sans-serif;
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
&:hover {
color: #333;
}
color: var(--bb-title, #333);
}
}
@@ -80,7 +65,8 @@
.promotion-prompt {
margin: 0 0 20px 0;
text-align: center;
color: #666;
color: var(--bb-title);
opacity: 0.8;
font-size: 14px;
}
@@ -89,43 +75,3 @@
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.promotion-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
background: #f5f5f5;
border: 2px solid #ddd;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
&:hover {
background: #e8e8e8;
border-color: #999;
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
box-shadow: none;
}
.piece-symbol {
font-size: 32px;
line-height: 1;
}
.piece-label {
font-size: 12px;
font-weight: 500;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
@@ -2,25 +2,22 @@
<div class="promotion-dialog">
<div class="promotion-header">
<h3>Pawn Promotion</h3>
<button class="close-btn" (click)="close()" aria-label="Close">&times;</button>
<button class="app-btn" (click)="close()" aria-label="Close"
style="padding:0.2rem 0.5rem;min-width:unset;">&times;</button>
</div>
<div class="promotion-body">
<p class="promotion-prompt">Choose a piece to promote your pawn to:</p>
<div class="promotion-options">
@for (piece of promotionPieces; track piece.type) {
<button
class="promotion-button"
[attr.data-piece]="piece.type"
(click)="selectPromotion(piece.type)"
[title]="piece.label"
>
<span class="piece-symbol">{{ piece.symbol }}</span>
<span class="piece-label">{{ piece.label }}</span>
</button>
<button type="button" class="app-btn promotion-choice" [attr.data-piece]="piece.type"
(click)="selectPromotion(piece.type)" [title]="piece.label">
<span class="piece-symbol">{{ piece.symbol }}</span>
<span class="piece-label">{{ piece.label }}</span>
</button>
}
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,68 @@
@import '../../button-template.css';
.dialog-overlay {
position: fixed;
inset: 0;
background: rgba(2, 2, 10, 0.58);
display: grid;
place-items: center;
z-index: 350;
padding: 1rem;
}
.dialog-card {
width: min(460px, 100%);
background: var(--dlg-bg);
border: 1.5px solid var(--dlg-border);
box-shadow: var(--bb-glow);
border-radius: 4px;
padding: 1rem;
display: grid;
gap: 0.7rem;
}
.dialog-title {
font-family: 'Bebas Neue', sans-serif;
font-size: 22px;
letter-spacing: 2px;
color: var(--bb-title);
text-align: center;
}
.dialog-input {
width: 100%;
background: rgba(4, 4, 20, 0.62);
border: 1px solid var(--bb-border);
color: var(--bb-title);
border-radius: 2px;
padding: 0.6rem 0.7rem;
font-family: 'Space Mono', monospace;
font-size: 13px;
}
.dialog-input:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(0, 213, 255, 0.25);
}
.dialog-textarea {
min-height: 120px;
resize: vertical;
}
.text-danger {
color: #dc3545;
font-size: 0.875rem;
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
@@ -0,0 +1,43 @@
<div class="dialog-overlay" (click)="closeDialog()">
<div class="dialog-card" (click)="$event.stopPropagation()">
<div class="dialog-title">CREATE ACCOUNT</div>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<input id="username" type="text" class="dialog-input" formControlName="username" placeholder="Username"
[disabled]="isLoading" />
@if (registerForm.get('username')?.invalid && registerForm.get('username')?.touched) {
<small class="text-danger">Username must be at least 3 characters</small>
}
<input id="email" type="email" class="dialog-input" formControlName="email" placeholder="Email"
[disabled]="isLoading" />
@if (registerForm.get('email')?.invalid && registerForm.get('email')?.touched) {
<small class="text-danger">Please enter a valid email</small>
}
<input id="password" type="password" class="dialog-input" formControlName="password" placeholder="Password"
[disabled]="isLoading" />
@if (registerForm.get('password')?.invalid && registerForm.get('password')?.touched) {
<small class="text-danger">Password must be at least 6 characters</small>
}
<input id="confirmPassword" type="password" class="dialog-input" formControlName="confirmPassword"
placeholder="Confirm Password" [disabled]="isLoading" />
@if (errorMessage) {
<div class="error-banner">{{ errorMessage }}</div>
}
<div class="dialog-actions">
<button type="button" class="app-btn" (click)="openLogin()">Already have an account?</button>
<button type="button" class="app-btn" (click)="closeDialog()">Cancel</button>
<button type="submit" class="app-btn" [disabled]="isLoading || registerForm.invalid">
@if (isLoading) {
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
}
Register
</button>
</div>
</form>
</div>
</div>
@@ -0,0 +1,71 @@
import { Component, EventEmitter, Output, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service';
import { AuthDialogService } from '../../services/auth-dialog.service';
@Component({
selector: 'app-register-dialog',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './register-dialog.component.html',
styleUrl: './register-dialog.component.css'
})
export class RegisterDialogComponent {
@Output() onClose = new EventEmitter<void>();
@Output() onSuccess = new EventEmitter<void>();
private readonly authService = inject(AuthService);
private readonly authDialogService = inject(AuthDialogService);
private readonly formBuilder = inject(FormBuilder);
registerForm: FormGroup;
errorMessage: string | null = null;
isLoading = false;
constructor() {
this.registerForm = this.formBuilder.group({
username: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', [Validators.required]]
});
}
onSubmit(): void {
if (this.registerForm.invalid) {
this.errorMessage = 'Please fill in all fields correctly';
return;
}
const { password, confirmPassword } = this.registerForm.value;
if (password !== confirmPassword) {
this.errorMessage = 'Passwords do not match';
return;
}
this.isLoading = true;
this.errorMessage = null;
const { username, email, password: pwd } = this.registerForm.value;
this.authService.register(username, pwd, email).subscribe({
next: () => {
this.isLoading = false;
this.onSuccess.emit();
},
error: (err) => {
this.isLoading = false;
this.errorMessage =
err.error?.message || 'Registration failed. Please try again.';
}
});
}
closeDialog(): void {
this.onClose.emit();
}
openLogin(): void {
this.authDialogService.openLogin();
}
}
@@ -0,0 +1,84 @@
@import '../../button-template.css';
.navbar {
background: rgba(8, 6, 28, 0.85);
backdrop-filter: blur(8px);
box-shadow: 0 4px 20px rgba(0, 210, 255, 0.15);
border-bottom: 1px solid rgba(0, 210, 255, 0.2);
border-radius: 0;
padding: 0.75rem 1rem;
}
.navbar-brand {
font-size: 1.5rem;
font-weight: bold;
color: var(--bb-title) !important;
font-family: 'Bebas Neue', sans-serif;
letter-spacing: 1px;
cursor: pointer;
}
.gap-2 {
gap: 0.5rem;
}
.user-section {
align-items: center;
}
.me-btn {
background: rgba(0, 210, 255, 0.1);
color: var(--bb-title);
border: 1px solid var(--bb-border);
border-radius: 2px;
padding: 0.5rem 0.8rem;
font-family: 'Space Mono', monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
outline: none;
text-transform: uppercase;
}
.me-btn:hover {
background: rgba(0, 210, 255, 0.2);
border-color: var(--bb-tag);
box-shadow: 0 0 10px rgba(0, 210, 255, 0.4);
transform: scale(1.05);
}
.me-btn:active {
transform: scale(0.98);
}
/* Sunset Mode */
.sunset .navbar {
background: rgba(20, 5, 45, 0.85);
border-bottom-color: rgba(255, 64, 207, 0.2);
box-shadow: 0 4px 20px rgba(242, 106, 226, 0.15);
}
.sunset .me-btn {
background: rgba(242, 106, 226, 0.1);
border-color: var(--bb-border);
}
.sunset .me-btn:hover {
background: rgba(242, 106, 226, 0.2);
border-color: var(--bb-tag);
box-shadow: 0 0 10px rgba(242, 106, 226, 0.4);
}
.container-fluid {
display: flex;
align-items: center;
}
.ms-auto {
margin-left: auto;
}
@@ -0,0 +1,35 @@
<nav class="navbar">
<div class="container-fluid">
<span class="navbar-brand">NowChess</span>
<div class="ms-auto">
<div class="d-flex align-items-center gap-2">
<button type="button" class="app-btn" (click)="toggleTheme()">
{{ isDarkMode ? 'Light mode' : 'Dark mode' }}
</button>
@if (currentUser; as user) {
<div class="d-flex align-items-center gap-2 user-section">
<button type="button" class="me-btn" (click)="goToProfile()">
👤 {{ user.username }}
</button>
<button type="button" class="app-btn" (click)="logout()">Logout</button>
</div>
} @else {
<button type="button" class="app-btn" (click)="openLoginDialog()">
Login
</button>
<button type="button" class="app-btn" (click)="openRegisterDialog()">
Register
</button>
}
</div>
</div>
</div>
</nav>
@if (showLoginDialog) {
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
}
@if (showRegisterDialog) {
<app-register-dialog (onClose)="closeRegisterDialog()" (onSuccess)="onRegisterSuccess()" />
}
@@ -0,0 +1,87 @@
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AuthService } from '../../services/auth.service';
import { AuthDialogService } from '../../services/auth-dialog.service';
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';
@Component({
selector: 'app-toolbar',
standalone: true,
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent],
templateUrl: './toolbar.component.html',
styleUrl: './toolbar.component.css'
})
export class ToolbarComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly authService = inject(AuthService);
private readonly authDialogService = inject(AuthDialogService);
private readonly themeService = inject(ThemeService);
private readonly router = inject(Router);
currentUser: CurrentUser | null = null;
showLoginDialog = false;
showRegisterDialog = false;
isDarkMode = false;
ngOnInit(): void {
this.authService.currentUser$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.currentUser = user;
});
this.authDialogService.dialogState$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
this.showLoginDialog = state === 'login';
this.showRegisterDialog = state === 'register';
});
this.themeService.darkMode$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((isDarkMode) => {
this.isDarkMode = isDarkMode;
});
}
openLoginDialog(): void {
this.authDialogService.openLogin();
}
closeLoginDialog(): void {
this.authDialogService.close();
}
openRegisterDialog(): void {
this.authDialogService.openRegister();
}
closeRegisterDialog(): void {
this.authDialogService.close();
}
logout(): void {
this.authService.logout();
}
toggleTheme(): void {
this.themeService.toggleTheme();
}
goToProfile(): void {
this.router.navigate(['/profile']);
}
onLoginSuccess(): void {
this.closeLoginDialog();
}
onRegisterSuccess(): void {
this.closeRegisterDialog();
}
}