feat: NCWF-5/6/7/8/9 chess analysis page and engine integration
- NCWF-5: scaffold /analysis route with ChessBoard viewer and navigation - NCWF-6: FEN / PGN / Game-ID input form with depth selector - NCWF-7: extend GameApiService with analyzePosition(); add AnalysisService with game-wide annotation pipeline; proxy /api/analysis -> :8087 - NCWF-8: EvalTimelineComponent — SVG win-chance chart per ply - NCWF-9: AnnotatedMoveListComponent — quality labels (!! ! ?! ? ??) derived from win-chance delta Also fix pre-existing app.spec.ts failure (missing provideHttpClient). Apply project-wide prettier formatting pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
<nav class="nc-nav">
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="nc-logo" (click)="goToHome()" role="button" tabindex="0">
|
||||
<div class="nc-logo-mark">♞</div>
|
||||
@@ -8,201 +7,315 @@
|
||||
|
||||
<!-- Center links — only when logged in -->
|
||||
@if (currentUser) {
|
||||
<div class="nc-links">
|
||||
<button type="button" class="nc-link">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
Watch
|
||||
</button>
|
||||
<button type="button" class="nc-link" (click)="goToTournaments()">Tournaments</button>
|
||||
<button type="button" class="nc-link" (click)="goToBots()">Bots</button>
|
||||
</div>
|
||||
<div class="nc-links">
|
||||
<button type="button" class="nc-link">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
Watch
|
||||
</button>
|
||||
<button type="button" class="nc-link" (click)="goToTournaments()">Tournaments</button>
|
||||
<button type="button" class="nc-link" (click)="goToBots()">Bots</button>
|
||||
<button type="button" class="nc-link" (click)="goToAnalysis()">Analysis</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Right cluster -->
|
||||
<div class="nc-right">
|
||||
|
||||
@if (currentUser; as user) {
|
||||
<!-- Notifications bell -->
|
||||
<div class="nc-dropdown-wrap" data-dropdown="notif">
|
||||
<button
|
||||
type="button"
|
||||
class="nc-bell"
|
||||
[class.is-open]="notifOpen"
|
||||
(click)="toggleNotif($event)"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
||||
</svg>
|
||||
@if (incomingChallenges.length > 0) {
|
||||
<span class="nc-badge">{{ incomingChallenges.length }}</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
<!-- Notifications bell -->
|
||||
<div class="nc-dropdown-wrap" data-dropdown="notif">
|
||||
<button type="button" class="nc-bell" [class.is-open]="notifOpen" (click)="toggleNotif($event)"
|
||||
aria-label="Notifications">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
||||
</svg>
|
||||
@if (incomingChallenges.length > 0) {
|
||||
<span class="nc-badge">{{ incomingChallenges.length }}</span>
|
||||
@if (notifOpen) {
|
||||
<div class="nc-popover nc-notif" (click)="$event.stopPropagation()">
|
||||
<div class="nc-notif-header">
|
||||
<span class="nc-notif-header-title">Challenges</span>
|
||||
</div>
|
||||
|
||||
<div class="nc-notif-list">
|
||||
@if (incomingChallenges.length === 0) {
|
||||
<div class="nc-notif-empty">No pending challenges</div>
|
||||
}
|
||||
@for (challenge of incomingChallenges; track challenge.id) {
|
||||
<div class="nc-notif-row is-unread">
|
||||
<div class="nc-notif-icon">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="14.5" y1="17.5" x2="3" y2="6" />
|
||||
<path d="M13 19l6 -6" />
|
||||
<path d="M16 16l4 4" />
|
||||
<path d="M19 21l2 -2" />
|
||||
<path d="M15 5l4 4" />
|
||||
<path d="M21 3l-3 1l-4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="nc-notif-body">
|
||||
<div class="nc-notif-text">
|
||||
<b>{{ challenge.challenger.name }}</b> challenged you to a
|
||||
{{ getTimeControlDisplay(challenge) }} game.
|
||||
</div>
|
||||
<div class="nc-notif-meta">
|
||||
{{ challenge.challenger.rating }} ·
|
||||
{{ challenge.timeControl.type ?? 'Custom' }} ·
|
||||
{{ getExpirationInfo(challenge) }}
|
||||
</div>
|
||||
<div class="nc-notif-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="nc-btn-accept"
|
||||
[disabled]="acceptingId === challenge.id || !!decliningId"
|
||||
(click)="acceptChallenge($event, challenge)"
|
||||
>
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{{ acceptingId === challenge.id ? '...' : 'Accept' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="nc-btn-decline"
|
||||
[disabled]="!!acceptingId || decliningId === challenge.id"
|
||||
(click)="declineChallenge($event, challenge)"
|
||||
>
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
{{ decliningId === challenge.id ? '...' : 'Decline' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="nc-notif-footer">
|
||||
<button type="button" class="nc-view-all" (click)="goToChallenges()">
|
||||
View all challenges
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (notifOpen) {
|
||||
<div class="nc-popover nc-notif" (click)="$event.stopPropagation()">
|
||||
<div class="nc-notif-header">
|
||||
<span class="nc-notif-header-title">Challenges</span>
|
||||
</div>
|
||||
|
||||
<div class="nc-notif-list">
|
||||
@if (incomingChallenges.length === 0) {
|
||||
<div class="nc-notif-empty">No pending challenges</div>
|
||||
}
|
||||
@for (challenge of incomingChallenges; track challenge.id) {
|
||||
<div class="nc-notif-row is-unread">
|
||||
<div class="nc-notif-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="14.5" y1="17.5" x2="3" y2="6" />
|
||||
<path d="M13 19l6 -6" /><path d="M16 16l4 4" />
|
||||
<path d="M19 21l2 -2" /><path d="M15 5l4 4" />
|
||||
<path d="M21 3l-3 1l-4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="nc-notif-body">
|
||||
<div class="nc-notif-text">
|
||||
<b>{{ challenge.challenger.name }}</b> challenged you to a
|
||||
{{ getTimeControlDisplay(challenge) }} game.
|
||||
</div>
|
||||
<div class="nc-notif-meta">
|
||||
{{ challenge.challenger.rating }} · {{ challenge.timeControl.type ?? 'Custom' }} · {{ getExpirationInfo(challenge) }}
|
||||
</div>
|
||||
<div class="nc-notif-actions">
|
||||
<button type="button" class="nc-btn-accept"
|
||||
[disabled]="acceptingId === challenge.id || !!decliningId"
|
||||
(click)="acceptChallenge($event, challenge)">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{{ acceptingId === challenge.id ? '...' : 'Accept' }}
|
||||
</button>
|
||||
<button type="button" class="nc-btn-decline"
|
||||
[disabled]="!!acceptingId || decliningId === challenge.id"
|
||||
(click)="declineChallenge($event, challenge)">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
{{ decliningId === challenge.id ? '...' : 'Decline' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="nc-notif-footer">
|
||||
<button type="button" class="nc-view-all" (click)="goToChallenges()">View all challenges</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Games quick-access -->
|
||||
<button type="button" class="nc-games-btn" (click)="goToGames()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.5 17.5L3 6"/>
|
||||
<path d="M13 19l6-6"/><path d="M16 16l4 4"/>
|
||||
<path d="M19 21l2-2"/><path d="M15 5l4 4"/>
|
||||
<path d="M21 3l-3 1-4 4"/>
|
||||
</svg>
|
||||
Games
|
||||
</button>
|
||||
|
||||
<!-- Profile -->
|
||||
<div class="nc-dropdown-wrap" data-dropdown="profile">
|
||||
<button type="button" class="nc-profile" [class.is-open]="profileOpen" (click)="toggleProfile($event)">
|
||||
<div class="nc-avatar nc-avatar-sm">{{ getInitial() }}</div>
|
||||
<span class="nc-profile-name">{{ user.username }}</span>
|
||||
<svg class="nc-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
<!-- Games quick-access -->
|
||||
<button type="button" class="nc-games-btn" (click)="goToGames()">
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14.5 17.5L3 6" />
|
||||
<path d="M13 19l6-6" />
|
||||
<path d="M16 16l4 4" />
|
||||
<path d="M19 21l2-2" />
|
||||
<path d="M15 5l4 4" />
|
||||
<path d="M21 3l-3 1-4 4" />
|
||||
</svg>
|
||||
Games
|
||||
</button>
|
||||
|
||||
@if (profileOpen) {
|
||||
<div class="nc-popover nc-menu" (click)="$event.stopPropagation()">
|
||||
<div class="nc-menu-header">
|
||||
<div class="nc-avatar nc-avatar-md">{{ getInitial() }}</div>
|
||||
<div>
|
||||
<div class="nc-menu-user-name">{{ user.username }}</div>
|
||||
<div class="nc-menu-user-sub">{{ user.rating }} · @{{ user.username }}</div>
|
||||
<!-- Profile -->
|
||||
<div class="nc-dropdown-wrap" data-dropdown="profile">
|
||||
<button
|
||||
type="button"
|
||||
class="nc-profile"
|
||||
[class.is-open]="profileOpen"
|
||||
(click)="toggleProfile($event)"
|
||||
>
|
||||
<div class="nc-avatar nc-avatar-sm">{{ getInitial() }}</div>
|
||||
<span class="nc-profile-name">{{ user.username }}</span>
|
||||
<svg
|
||||
class="nc-chevron"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@if (profileOpen) {
|
||||
<div class="nc-popover nc-menu" (click)="$event.stopPropagation()">
|
||||
<div class="nc-menu-header">
|
||||
<div class="nc-avatar nc-avatar-md">{{ getInitial() }}</div>
|
||||
<div>
|
||||
<div class="nc-menu-user-name">{{ user.username }}</div>
|
||||
<div class="nc-menu-user-sub">{{ user.rating }} · @{{ user.username }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-menu-group">
|
||||
<button type="button" class="nc-menu-item" (click)="goToProfile()">
|
||||
<span class="nc-menu-icon">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">My profile</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="nc-menu-item" (click)="goToChallenges()">
|
||||
<span class="nc-menu-icon">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="14.5" y1="17.5" x2="3" y2="6" />
|
||||
<path d="M13 19l6 -6" />
|
||||
<path d="M16 16l4 4" />
|
||||
<path d="M19 21l2 -2" />
|
||||
<path d="M15 5l4 4" />
|
||||
<path d="M21 3l-3 1l-4 4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">Challenges</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="nc-menu-item" (click)="toggleTheme($event)">
|
||||
<span class="nc-menu-icon">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">{{ isDarkMode ? 'Light mode' : 'Dark mode' }}</span>
|
||||
<span class="nc-toggle" [class.is-on]="isDarkMode"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nc-menu-group">
|
||||
<button type="button" class="nc-menu-item danger" (click)="logout()">
|
||||
<span class="nc-menu-icon">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nc-menu-group">
|
||||
<button type="button" class="nc-menu-item" (click)="goToProfile()">
|
||||
<span class="nc-menu-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">My profile</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="nc-menu-item" (click)="goToChallenges()">
|
||||
<span class="nc-menu-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="14.5" y1="17.5" x2="3" y2="6" />
|
||||
<path d="M13 19l6 -6" /><path d="M16 16l4 4" />
|
||||
<path d="M19 21l2 -2" /><path d="M15 5l4 4" />
|
||||
<path d="M21 3l-3 1l-4 4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">Challenges</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="nc-menu-item" (click)="toggleTheme($event)">
|
||||
<span class="nc-menu-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">{{ isDarkMode ? 'Light mode' : 'Dark mode' }}</span>
|
||||
<span class="nc-toggle" [class.is-on]="isDarkMode"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nc-menu-group">
|
||||
<button type="button" class="nc-menu-item danger" (click)="logout()">
|
||||
<span class="nc-menu-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nc-menu-label">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
} @else {
|
||||
|
||||
<!-- Logged-out auth buttons -->
|
||||
<button type="button" class="nc-auth-btn" (click)="openLoginDialog()">Login</button>
|
||||
<button type="button" class="nc-auth-btn nc-auth-btn--primary" (click)="openRegisterDialog()">Register</button>
|
||||
|
||||
<!-- Logged-out auth buttons -->
|
||||
<button type="button" class="nc-auth-btn" (click)="openLoginDialog()">Login</button>
|
||||
<button type="button" class="nc-auth-btn nc-auth-btn--primary" (click)="openRegisterDialog()">
|
||||
Register
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@if (showLoginDialog) {
|
||||
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
|
||||
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
|
||||
}
|
||||
|
||||
@if (showRegisterDialog) {
|
||||
<app-register-dialog (onClose)="closeRegisterDialog()" (onSuccess)="onRegisterSuccess()" />
|
||||
<app-register-dialog (onClose)="closeRegisterDialog()" (onSuccess)="onRegisterSuccess()" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user