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`;