diff --git a/src/app/pages/game/game.component.css b/src/app/pages/game/game.component.css index 8e32907..7b5ad50 100644 --- a/src/app/pages/game/game.component.css +++ b/src/app/pages/game/game.component.css @@ -45,6 +45,21 @@ h2 { flex: 0 0 auto; } +.content-layout { + flex: 1 1 auto; + min-height: 0; + display: grid; + grid-template-columns: minmax(220px, 0.95fr) minmax(0, 2.4fr) minmax(220px, 0.95fr); + gap: 0.75rem; +} + +.center-column { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; +} + .move-form { display: flex; flex-wrap: wrap; @@ -59,7 +74,7 @@ h2 { .board-section { flex: 1 1 auto; - min-height: 0; + min-height: 180px; display: grid; place-items: center; container-type: size; @@ -70,6 +85,22 @@ h2 { overflow: hidden; } +.import-card { + background: #E1EAA9; + border: 2px solid #5A2C28; + border-radius: 10px; + padding: 0.65rem; + display: grid; + align-self: start; + align-content: start; + gap: 0.5rem; +} + +.import-card label { + color: #5A2C28; + font-weight: 700; +} + input { border: 2px solid #5A2C28; border-radius: 10px; @@ -78,6 +109,15 @@ input { min-width: 180px; } +textarea { + border: 2px solid #5A2C28; + border-radius: 10px; + background: #B9DAD1; + padding: 0.6rem 0.75rem; + resize: vertical; + min-height: 80px; +} + button { border: 2px solid #5A2C28; border-radius: 10px; @@ -97,3 +137,25 @@ button:hover { color: #5A2C28; font-weight: 700; } + +@media (max-width: 920px) { + .content-layout { + grid-template-columns: 1fr; + grid-template-areas: + "center" + "fen" + "pgn"; + } + + .center-column { + grid-area: center; + } + + .fen-card { + grid-area: fen; + } + + .pgn-card { + grid-area: pgn; + } +} diff --git a/src/app/pages/game/game.component.html b/src/app/pages/game/game.component.html index 05559dc..05397c4 100644 --- a/src/app/pages/game/game.component.html +++ b/src/app/pages/game/game.component.html @@ -9,22 +9,50 @@ @if (loading) {

Loading game state...

} @else if (state) { -
-
- - - -
-

Click your piece to highlight legal targets.

-
+
+ -
- +
+
+
+ + + +
+

Click your piece to highlight legal targets.

+
+ +
+ +
+
+ +
} diff --git a/src/app/pages/game/game.component.ts b/src/app/pages/game/game.component.ts index d376a7a..28f130d 100644 --- a/src/app/pages/game/game.component.ts +++ b/src/app/pages/game/game.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute, RouterLink } from '@angular/router'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { interval, startWith, Subscription, switchMap } from 'rxjs'; import { ChessBoardComponent } from '../../components/chess-board/chess-board.component'; import { GameFull, GameState, GameStreamEvent, LegalMove } from '../../models/game.models'; @@ -19,6 +19,8 @@ export class GameComponent implements OnInit, OnDestroy { game: GameFull | null = null; errorMessage = ''; moveInput = ''; + fenInput = ''; + pgnInput = ''; loading = true; selectedSquare: string | null = null; highlightedSquares: string[] = []; @@ -30,6 +32,7 @@ export class GameComponent implements OnInit, OnDestroy { constructor( private readonly route: ActivatedRoute, + private readonly router: Router, private readonly gameApi: GameApiService ) {} @@ -112,6 +115,46 @@ export class GameComponent implements OnInit, OnDestroy { }); } + importFen(): void { + const fen = this.fenInput.trim(); + if (!fen) { + this.errorMessage = 'Please provide a FEN string.'; + return; + } + + this.errorMessage = ''; + this.gameApi.importFen(fen).subscribe({ + next: (game) => { + this.fenInput = ''; + this.pgnInput = ''; + void this.router.navigate(['/game', game.gameId]); + }, + error: (error: { error?: { message?: string } }) => { + this.errorMessage = error.error?.message ?? 'FEN import failed.'; + } + }); + } + + importPgn(): void { + const pgn = this.pgnInput.trim(); + if (!pgn) { + this.errorMessage = 'Please provide a PGN string.'; + return; + } + + this.errorMessage = ''; + this.gameApi.importPgn(pgn).subscribe({ + next: (game) => { + this.pgnInput = ''; + this.fenInput = ''; + void this.router.navigate(['/game', game.gameId]); + }, + error: (error: { error?: { message?: string } }) => { + this.errorMessage = error.error?.message ?? 'PGN import failed.'; + } + }); + } + private loadGame(): void { this.loading = true; this.errorMessage = ''; diff --git a/src/app/services/game-api.service.ts b/src/app/services/game-api.service.ts index cfcb315..5508da3 100644 --- a/src/app/services/game-api.service.ts +++ b/src/app/services/game-api.service.ts @@ -37,6 +37,14 @@ export class GameApiService { return this.http.get(`${this.apiBase}/api/board/game/${gameId}/moves`, { params }); } + importFen(fen: string): Observable { + return this.http.post(`${this.apiBase}/api/board/game/import/fen`, { fen }); + } + + importPgn(pgn: string): Observable { + return this.http.post(`${this.apiBase}/api/board/game/import/pgn`, { pgn }); + } + streamGame(gameId: string): Observable { return new Observable((observer) => { const wsUrl = `${this.wsBase}/api/board/game/${gameId}/stream`;