fix: NCWF-4 Token Issues (#8)
Co-authored-by: Lala, Shahd <Shahd.Lala@sybit.de> Co-authored-by: shahdlala66 <shahd.lala66@gmail.com> Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
@@ -28,30 +28,30 @@
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
LIGHT MODE TOKEN OVERRIDES
|
||||
LIGHT MODE TOKEN OVERRIDES (sunset-gradient palette)
|
||||
============================================================ */
|
||||
:host-context(html:not([data-theme='dark'])) {
|
||||
--nc-neon: #c026d3;
|
||||
--nc-neon-soft: rgba(192, 38, 211, 0.45);
|
||||
--nc-neon-clock-bg: rgba(192, 38, 211, 0.07);
|
||||
--nc-bg: #f5f0fc;
|
||||
--nc-surface: rgba(255, 255, 255, 0.88);
|
||||
--nc-surface-solid: rgba(255, 255, 255, 0.98);
|
||||
--nc-text: #0f0022;
|
||||
--nc-text-muted: rgba(15, 0, 34, 0.65);
|
||||
--nc-text-dim: rgba(15, 0, 34, 0.40);
|
||||
--nc-border: rgba(15, 0, 34, 0.10);
|
||||
--nc-border-strong: rgba(15, 0, 34, 0.20);
|
||||
--nc-warning: #d97706;
|
||||
--nc-warning-soft: rgba(217, 119, 6, 0.35);
|
||||
--nc-danger: #dc2626;
|
||||
--nc-danger-soft: rgba(220, 38, 38, 0.25);
|
||||
--nc-danger-bg: rgba(220, 38, 38, 0.06);
|
||||
--nc-success: #059669;
|
||||
--nc-clock-bg: rgba(0, 0, 0, 0.04);
|
||||
--nc-btn-bg: rgba(0, 0, 0, 0.03);
|
||||
--nc-btn-hover-bg: rgba(0, 0, 0, 0.06);
|
||||
--nc-seg-bg: rgba(0, 0, 0, 0.06);
|
||||
--nc-neon: #ff3dbb;
|
||||
--nc-neon-soft: rgba(255, 61, 187, 0.55);
|
||||
--nc-neon-clock-bg: rgba(255, 61, 187, 0.08);
|
||||
--nc-bg: transparent;
|
||||
--nc-surface: rgba(26, 24, 56, 0.72);
|
||||
--nc-surface-solid: rgba(26, 24, 56, 0.97);
|
||||
--nc-text: #fff;
|
||||
--nc-text-muted: rgba(255, 255, 255, 0.72);
|
||||
--nc-text-dim: rgba(255, 255, 255, 0.45);
|
||||
--nc-border: rgba(255, 255, 255, 0.10);
|
||||
--nc-border-strong: rgba(255, 255, 255, 0.18);
|
||||
--nc-warning: #ffb13a;
|
||||
--nc-warning-soft: rgba(255, 177, 58, 0.40);
|
||||
--nc-danger: #ff7a7a;
|
||||
--nc-danger-soft: rgba(255, 122, 122, 0.30);
|
||||
--nc-danger-bg: rgba(255, 122, 122, 0.08);
|
||||
--nc-success: #5ee5a1;
|
||||
--nc-clock-bg: rgba(0, 0, 0, 0.30);
|
||||
--nc-btn-bg: rgba(255, 255, 255, 0.05);
|
||||
--nc-btn-hover-bg: rgba(255, 255, 255, 0.10);
|
||||
--nc-seg-bg: rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -78,8 +78,8 @@
|
||||
|
||||
:host-context(html:not([data-theme='dark'])) .game-shell::before {
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 20% 100%, rgba(192, 38, 211, 0.06), transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 90% 0%, rgba(120, 40, 180, 0.08), transparent 60%);
|
||||
radial-gradient(ellipse 80% 50% at 20% 100%, rgba(212, 77, 74, 0.10), transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 90% 0%, rgba(74, 41, 98, 0.22), transparent 60%);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -290,12 +290,53 @@
|
||||
animation: slideIn 0.35s ease-out;
|
||||
}
|
||||
|
||||
.completion-banner--timeout {
|
||||
background: rgba(255, 177, 58, 0.06);
|
||||
border-color: var(--nc-warning-soft);
|
||||
}
|
||||
|
||||
.completion-banner--timeout .completion-title {
|
||||
color: var(--nc-warning);
|
||||
}
|
||||
|
||||
.completion-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.completion-icon {
|
||||
font-size: 22px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.completion-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.completion-new {
|
||||
font-size: 11px !important;
|
||||
padding: 8px 14px !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.completion-title { font-size: 16px; font-weight: 600; color: var(--nc-neon); }
|
||||
.completion-title { font-size: 15px; font-weight: 700; color: var(--nc-neon); }
|
||||
|
||||
.completion-sub {
|
||||
font-family: var(--nc-mono);
|
||||
font-size: 10px;
|
||||
color: var(--nc-text-dim);
|
||||
letter-spacing: 0.08em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.completion-link {
|
||||
font-family: var(--nc-mono);
|
||||
@@ -345,8 +386,8 @@
|
||||
}
|
||||
|
||||
:host-context(html:not([data-theme='dark'])) .status-strip {
|
||||
background: rgba(192, 38, 211, 0.04);
|
||||
border-color: rgba(192, 38, 211, 0.18);
|
||||
background: rgba(255, 61, 187, 0.06);
|
||||
border-color: rgba(255, 61, 187, 0.20);
|
||||
}
|
||||
|
||||
.status-left { display: inline-flex; align-items: center; gap: 10px; }
|
||||
@@ -388,7 +429,12 @@
|
||||
}
|
||||
|
||||
:host-context(html:not([data-theme='dark'])) .board-wrap {
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 61, 187, 0.08);
|
||||
}
|
||||
|
||||
.board-wrap.reviewing {
|
||||
border-color: var(--nc-warning-soft);
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 177, 58, 0.18);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -490,6 +536,60 @@
|
||||
|
||||
.seg-btn.active { background: var(--nc-neon); color: #fff; font-weight: 700; }
|
||||
|
||||
/* ============================================================
|
||||
RESIGN CONFIRM OVERLAY
|
||||
============================================================ */
|
||||
.confirm-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 600;
|
||||
}
|
||||
|
||||
.confirm-box {
|
||||
background: var(--nc-surface-solid);
|
||||
border: 1px solid var(--nc-danger-soft);
|
||||
padding: 28px 32px;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--nc-text);
|
||||
}
|
||||
|
||||
.confirm-sub {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--nc-text-muted);
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-danger-solid {
|
||||
background: var(--nc-danger) !important;
|
||||
color: #fff !important;
|
||||
border-color: var(--nc-danger) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-danger-solid:hover { opacity: 0.88; }
|
||||
|
||||
/* ============================================================
|
||||
TOAST
|
||||
============================================================ */
|
||||
|
||||
@@ -72,8 +72,24 @@
|
||||
<!-- Game completed banner -->
|
||||
@if (facade.isGameFinished && facade.gameCompletionMessage) {
|
||||
<div class="completion-banner">
|
||||
<span class="completion-title">{{ facade.gameCompletionMessage }}</span>
|
||||
<a routerLink="/" class="completion-link">Start new game</a>
|
||||
<div class="completion-left">
|
||||
<span class="completion-icon">♟</span>
|
||||
<div>
|
||||
<div class="completion-title">{{ facade.gameCompletionMessage }}</div>
|
||||
<div class="completion-sub">Game #{{ facade.gameId }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="completion-actions">
|
||||
<a routerLink="/games" class="completion-link">My games</a>
|
||||
<a routerLink="/" class="btn btn-primary completion-new">New game</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!facade.isGameFinished && ((whiteTimerMs !== null && whiteTimerMs <= 0) || (blackTimerMs !== null && blackTimerMs <= 0))) {
|
||||
<div class="completion-banner completion-banner--timeout">
|
||||
<span class="completion-title">Time's up!</span>
|
||||
<span class="completion-sub">Waiting for server to confirm result…</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -104,11 +120,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Board -->
|
||||
<div class="board-wrap">
|
||||
<div class="board-wrap" [class.reviewing]="facade.isReviewing">
|
||||
<app-chess-board
|
||||
[fen]="facade.state.fen"
|
||||
[selectedSquare]="facade.selectedSquare"
|
||||
[highlightedSquares]="facade.highlightedSquares"
|
||||
[fen]="facade.displayFen"
|
||||
[selectedSquare]="facade.isReviewing ? null : facade.selectedSquare"
|
||||
[highlightedSquares]="facade.isReviewing ? [] : facade.highlightedSquares"
|
||||
[boardTheme]="boardTheme"
|
||||
(squareSelected)="facade.onBoardSquareSelected($event)" />
|
||||
</div>
|
||||
@@ -146,7 +162,9 @@
|
||||
</summary>
|
||||
<app-move-history
|
||||
[moves]="facade.state.moves"
|
||||
(navigate)="onMoveNavigate($event)" />
|
||||
[viewingPly]="facade.viewingPly"
|
||||
(navigate)="facade.navigateHistory($event)"
|
||||
(navigateToPly)="facade.navigateToPly($event)" />
|
||||
</details>
|
||||
|
||||
<!-- Play move (collapsible) -->
|
||||
@@ -200,6 +218,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resign confirmation dialog -->
|
||||
@if (facade.resignConfirmPending) {
|
||||
<div class="confirm-overlay" role="dialog" aria-modal="true" aria-label="Confirm resign">
|
||||
<div class="confirm-box">
|
||||
<p class="confirm-title">Resign this game?</p>
|
||||
<p class="confirm-sub">Your opponent will be declared the winner.</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn" type="button" (click)="facade.cancelResign()">Cancel</button>
|
||||
<button class="btn btn-danger-solid" type="button" (click)="facade.confirmResign()">Yes, resign</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Toast notification -->
|
||||
@if (toastMessage) {
|
||||
<div class="toast show">{{ toastMessage }}</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { BoardActionsBarComponent } from '../../components/board-actions-bar/board-actions-bar.component';
|
||||
import { ChessBoardComponent } from '../../components/chess-board/chess-board.component';
|
||||
import { ExportPanelComponent } from '../../components/export-panel/export-panel.component';
|
||||
import { MoveHistoryComponent, MoveNavDirection } from '../../components/move-history/move-history.component';
|
||||
import { MoveHistoryComponent } from '../../components/move-history/move-history.component';
|
||||
import { PlayerCardComponent } from '../../components/player-card/player-card.component';
|
||||
import { PromotionDialogComponent } from '../../components/promotion-dialog/promotion-dialog.component';
|
||||
import { GameFacade } from './game.facade';
|
||||
@@ -158,12 +158,7 @@ export class GameComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onResign(): void {
|
||||
this.showToast('Resigned');
|
||||
}
|
||||
|
||||
// ── Move history navigation ───────────────────────────────────
|
||||
onMoveNavigate(_direction: MoveNavDirection): void {
|
||||
// Visual-only for now; board always reflects live position.
|
||||
this.facade.requestResign();
|
||||
}
|
||||
|
||||
// ── Timer helpers ─────────────────────────────────────────────
|
||||
@@ -199,6 +194,11 @@ export class GameComponent implements OnInit, OnDestroy {
|
||||
this.blackTimerMs = clock.blackRemainingMs < 0
|
||||
? -1
|
||||
: Math.max(0, clock.blackRemainingMs - (!activeIsWhite ? elapsed : 0));
|
||||
|
||||
if ((this.whiteTimerMs !== null && this.whiteTimerMs <= 0 && clock.whiteRemainingMs > 0) ||
|
||||
(this.blackTimerMs !== null && this.blackTimerMs <= 0 && clock.blackRemainingMs > 0)) {
|
||||
this.facade.errorMessage = '';
|
||||
}
|
||||
}
|
||||
|
||||
private showToast(msg: string): void {
|
||||
|
||||
@@ -23,6 +23,11 @@ export class GameFacade implements OnDestroy {
|
||||
gameCompletionMessage = '';
|
||||
isGameFinished = false;
|
||||
isPromotionDialogOpen = false;
|
||||
resignConfirmPending = false;
|
||||
|
||||
private fenHistory: string[] = [];
|
||||
private sessionStartPly = 0;
|
||||
viewingPly: number | null = null;
|
||||
|
||||
private boardSelection: BoardSelection = {
|
||||
selectedSquare: null,
|
||||
@@ -52,6 +57,46 @@ export class GameFacade implements OnDestroy {
|
||||
return this.boardSelection.highlightedSquares;
|
||||
}
|
||||
|
||||
get displayFen(): string {
|
||||
if (this.viewingPly !== null) {
|
||||
const historyIndex = this.viewingPly - this.sessionStartPly;
|
||||
return this.fenHistory[historyIndex] ?? this.game?.state.fen ?? '';
|
||||
}
|
||||
return this.game?.state.fen ?? '';
|
||||
}
|
||||
|
||||
get isReviewing(): boolean {
|
||||
return this.viewingPly !== null;
|
||||
}
|
||||
|
||||
navigateToPly(ply: number): void {
|
||||
const historyIndex = ply - this.sessionStartPly;
|
||||
if (historyIndex < 0 || historyIndex >= this.fenHistory.length) return;
|
||||
this.viewingPly = ply;
|
||||
this.boardSelection = this.boardSelectionService.clearSelection();
|
||||
}
|
||||
|
||||
navigateHistory(direction: 'first' | 'prev' | 'next' | 'last'): void {
|
||||
const totalPly = this.sessionStartPly + this.fenHistory.length - 1;
|
||||
const current = this.viewingPly ?? totalPly;
|
||||
|
||||
let next: number;
|
||||
switch (direction) {
|
||||
case 'first': next = this.sessionStartPly; break;
|
||||
case 'prev': next = Math.max(this.sessionStartPly, current - 1); break;
|
||||
case 'next': next = Math.min(totalPly, current + 1); break;
|
||||
case 'last':
|
||||
default: next = totalPly; break;
|
||||
}
|
||||
|
||||
if (next === totalPly) {
|
||||
this.viewingPly = null;
|
||||
} else {
|
||||
this.viewingPly = next;
|
||||
}
|
||||
this.boardSelection = this.boardSelectionService.clearSelection();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.streamService.cleanup();
|
||||
this.botMoveService.cleanup();
|
||||
@@ -63,7 +108,7 @@ export class GameFacade implements OnDestroy {
|
||||
}
|
||||
|
||||
onBoardSquareSelected(square: string): void {
|
||||
if (!this.state) {
|
||||
if (!this.state || this.viewingPly !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,6 +168,8 @@ export class GameFacade implements OnDestroy {
|
||||
if (this.game) {
|
||||
this.game = { ...this.game, state };
|
||||
this.clockSyncedAt = Date.now();
|
||||
this.pushFen(state.fen);
|
||||
this.viewingPly = null;
|
||||
this.updateGameCompletion();
|
||||
}
|
||||
this.moveInput = '';
|
||||
@@ -171,6 +218,26 @@ export class GameFacade implements OnDestroy {
|
||||
this.pendingPromotionMoves = [];
|
||||
}
|
||||
|
||||
requestResign(): void {
|
||||
this.resignConfirmPending = true;
|
||||
}
|
||||
|
||||
cancelResign(): void {
|
||||
this.resignConfirmPending = false;
|
||||
}
|
||||
|
||||
confirmResign(): void {
|
||||
this.resignConfirmPending = false;
|
||||
this.gameApi
|
||||
.resignGame(this.gameId)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
error: (error) => {
|
||||
this.errorMessage = getErrorMessage(error, 'Could not resign.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
importFen(): void {
|
||||
this.errorMessage = '';
|
||||
this.importService.importFen(
|
||||
@@ -204,6 +271,8 @@ export class GameFacade implements OnDestroy {
|
||||
this.errorMessage = '';
|
||||
this.boardSelection = this.boardSelectionService.clearSelection();
|
||||
this.streamService.cleanup();
|
||||
this.fenHistory = [];
|
||||
this.viewingPly = null;
|
||||
|
||||
this.gameApi
|
||||
.getGame(this.gameId)
|
||||
@@ -213,6 +282,8 @@ export class GameFacade implements OnDestroy {
|
||||
this.game = game;
|
||||
this.clockSyncedAt = Date.now();
|
||||
this.loading = false;
|
||||
this.sessionStartPly = game.state.moves.length;
|
||||
this.fenHistory = [game.state.fen];
|
||||
this.updateGameCompletion();
|
||||
this.gameHistory.recordGame(this.gameId);
|
||||
this.startStreaming();
|
||||
@@ -237,7 +308,10 @@ export class GameFacade implements OnDestroy {
|
||||
if (event.type === 'gameFull') {
|
||||
this.game = event.game;
|
||||
this.clockSyncedAt = Date.now();
|
||||
this.boardSelection = this.boardSelectionService.clearSelection();
|
||||
this.pushFen(event.game.state.fen);
|
||||
if (this.viewingPly === null) {
|
||||
this.boardSelection = this.boardSelectionService.clearSelection();
|
||||
}
|
||||
this.updateGameCompletion();
|
||||
this.tryMakeBotMove();
|
||||
return;
|
||||
@@ -247,8 +321,9 @@ export class GameFacade implements OnDestroy {
|
||||
const moveCountBefore = this.game.state.moves.length;
|
||||
this.game = { ...this.game, state: event.state };
|
||||
this.clockSyncedAt = Date.now();
|
||||
this.pushFen(event.state.fen);
|
||||
this.updateGameCompletion();
|
||||
if (event.state.moves.length !== moveCountBefore) {
|
||||
if (event.state.moves.length !== moveCountBefore && this.viewingPly === null) {
|
||||
this.boardSelection = this.boardSelectionService.clearSelection();
|
||||
this.tryMakeBotMove();
|
||||
}
|
||||
@@ -260,6 +335,13 @@ export class GameFacade implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private pushFen(fen: string): void {
|
||||
const last = this.fenHistory[this.fenHistory.length - 1];
|
||||
if (last !== fen) {
|
||||
this.fenHistory.push(fen);
|
||||
}
|
||||
}
|
||||
|
||||
private tryMakeBotMove(): void {
|
||||
this.botMoveService.tryMakeBotMove(
|
||||
this.gameId,
|
||||
|
||||
Reference in New Issue
Block a user