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:
@@ -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">×</button>
|
||||
<button class="app-btn" (click)="close()" aria-label="Close"
|
||||
style="padding:0.2rem 0.5rem;min-width:unset;">×</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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user