diff --git a/src/app/pages/game/game.component.css b/src/app/pages/game/game.component.css index 042bcef..b9ca9ea 100644 --- a/src/app/pages/game/game.component.css +++ b/src/app/pages/game/game.component.css @@ -70,6 +70,130 @@ h2 { container-type: size; } +.timer-card, +.history-card, +.export-card { + background: var(--color-bg-card); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--size-lg-padding); + display: grid; + gap: var(--size-md); +} + +.timer-card h2, +.history-card h2, +.export-card h2 { + margin: 0; + font-size: 1.1rem; + color: var(--color-text-primary); +} + +.history-list { + margin: 0; + padding-left: 1.1rem; + display: grid; + gap: var(--size-xs); + max-height: 180px; + overflow: auto; +} + +.history-list li { + color: var(--color-text-primary); + display: flex; + gap: var(--size-sm); + align-items: baseline; +} + +.history-number { + font-weight: 700; + min-width: 1.8rem; +} + +.history-move { + font-family: monospace; +} + +.history-empty { + margin: 0; + color: var(--color-text-primary); +} + +.player-timer { + background: var(--color-bg-input); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--size-md-padding); +} + +.active-timer { + box-shadow: 0 0 0 3px rgba(185, 218, 209, 0.25); +} + +.timer-label { + margin: 0; + color: var(--color-text-primary); + font-weight: 600; +} + +.timer-value { + margin: var(--size-xs) 0 0; + color: var(--color-text-primary); + font-size: 1.35rem; + font-weight: 700; +} + +.export-mode-group { + display: flex; + gap: var(--size-lg); + flex-wrap: wrap; +} + +.export-mode-option { + display: inline-flex; + align-items: center; + gap: var(--size-sm); + color: var(--color-text-primary); + font-weight: 600; +} + +.export-mode-option input { + accent-color: var(--color-primary); +} + +.export-text { + width: 100%; + min-height: 140px; + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + background: var(--color-bg-input); + color: var(--color-text-primary); + padding: var(--size-md-padding); + resize: vertical; +} + +.export-button { + width: fit-content; + border: var(--button-border); + border-radius: var(--button-radius); + background: var(--color-bg-button); + color: var(--color-text-primary); + font-weight: 700; + padding: var(--button-padding); + cursor: pointer; +} + +.export-button:hover { + background: var(--color-bg-button-hover); + color: var(--color-text-button-hover); +} + +.export-note { + margin: 0; + color: var(--color-text-primary); + font-weight: 600; +} + .alert { border-radius: var(--border-radius-sm); border: var(--border-width) solid var(--color-border); diff --git a/src/app/pages/game/game.component.html b/src/app/pages/game/game.component.html index 04c93b2..e0df059 100644 --- a/src/app/pages/game/game.component.html +++ b/src/app/pages/game/game.component.html @@ -25,18 +25,19 @@ }
White
+{{ formatTimer(whiteTimerSeconds) }}
+Black
+{{ formatTimer(blackTimerSeconds) }}
+No moves yet.
+ } @else { +{{ exportNotice }}
+ } +Import game
+{{ errorMessage }}
} diff --git a/src/app/pages/welcome/welcome.component.ts b/src/app/pages/welcome/welcome.component.ts index 3f3c149..a948317 100644 --- a/src/app/pages/welcome/welcome.component.ts +++ b/src/app/pages/welcome/welcome.component.ts @@ -18,8 +18,12 @@ export class WelcomeComponent { errorMessage = ''; showDifficultySelector = false; showJoinGameForm = false; + showImportGameForm = false; gameIdInput = ''; joiningGame = false; + importing = false; + importMode: 'fen' | 'pgn' = 'fen'; + importText = ''; constructor( private readonly router: Router, @@ -81,16 +85,60 @@ export class WelcomeComponent { toggleDifficultySelector(): void { this.showDifficultySelector = !this.showDifficultySelector; this.showJoinGameForm = false; + this.showImportGameForm = false; this.errorMessage = ''; } toggleJoinGameForm(): void { this.showJoinGameForm = !this.showJoinGameForm; this.showDifficultySelector = false; + this.showImportGameForm = false; this.errorMessage = ''; this.gameIdInput = ''; } + toggleImportGameForm(): void { + this.showImportGameForm = !this.showImportGameForm; + this.showDifficultySelector = false; + this.showJoinGameForm = false; + this.errorMessage = ''; + + if (!this.showImportGameForm) { + this.importText = ''; + this.importMode = 'fen'; + } + } + + setImportMode(mode: 'fen' | 'pgn'): void { + this.importMode = mode; + this.errorMessage = ''; + } + + submitImportedGame(): void { + const trimmedImport = this.importText.trim(); + if (this.importing || !trimmedImport) { + return; + } + + this.errorMessage = ''; + this.importing = true; + + const importRequest = + this.importMode === 'fen' ? this.gameApi.importFen(trimmedImport) : this.gameApi.importPgn(trimmedImport); + + importRequest.pipe(finalize(() => (this.importing = false))).subscribe({ + next: (game) => { + this.importText = ''; + this.showImportGameForm = false; + void this.router.navigate(['/game', game.gameId]); + }, + error: (error) => { + const defaultMessage = this.importMode === 'fen' ? 'Unable to import FEN.' : 'Unable to import PGN.'; + this.errorMessage = getErrorMessage(error, defaultMessage); + } + }); + } + joinGame(): void { if (this.joiningGame || !this.gameIdInput.trim()) { return; diff --git a/src/styles.css b/src/styles.css index fa5a81b..520eacd 100644 --- a/src/styles.css +++ b/src/styles.css @@ -62,3 +62,254 @@ button, input { font: inherit; } + +.welcome-shell .import-game-form { + grid-column: 1 / -1; + background: var(--color-bg-card); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + padding: var(--size-lg-padding); + margin: var(--size-md) 0; + display: grid; + gap: var(--size-md); +} + +.welcome-shell .import-game-form p { + margin: 0; + font-weight: 600; + color: var(--color-text-primary); +} + +.welcome-shell .import-mode-group { + display: flex; + gap: var(--size-lg); + flex-wrap: wrap; +} + +.welcome-shell .import-mode-option { + display: inline-flex; + align-items: center; + gap: var(--size-sm); + font-weight: 600; + color: var(--color-text-primary); +} + +.welcome-shell .import-mode-option input { + accent-color: var(--color-primary); +} + +.welcome-shell .import-game-text { + width: 100%; + resize: vertical; + min-height: 110px; + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius-md); + background: var(--color-bg-input); + color: var(--color-text-primary); + padding: var(--size-md-padding); +} + +.welcome-shell .import-game-text:focus { + outline: none; + border-color: var(--color-secondary-mint); + box-shadow: 0 0 0 3px rgba(185, 218, 209, 0.2); +} + +.welcome-shell .theme-toggle-container { + position: absolute; + top: 20px; + right: 20px; + z-index: 100; +} + +.welcome-shell .switch { + display: inline-block; + position: relative; +} + +.welcome-shell .switch__input { + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + +.welcome-shell .switch__label { + position: relative; + display: inline-block; + width: 120px; + height: 60px; + background-color: #2B2B2B; + border: 5px solid #5B5B5B; + border-radius: 9999px; + cursor: pointer; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) translateX(-72%); + display: block; + width: 40px; + height: 40px; + background-color: #7B7B7B; + border-radius: 9999px; + box-shadow: 10px 0px 0 0 rgba(0, 0, 0, 0.2) inset; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__indicator::before, +.welcome-shell .switch__indicator::after { + position: absolute; + content: ''; + display: block; + background-color: #FFFFFF; + border-radius: 9999px; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__indicator::before { + top: 7px; + left: 7px; + width: 9px; + height: 9px; + opacity: 0.6; +} + +.welcome-shell .switch__indicator::after { + bottom: 8px; + right: 6px; + width: 14px; + height: 14px; + opacity: 0.8; +} + +.welcome-shell .switch__decoration { + position: absolute; + top: 65%; + left: 50%; + display: block; + width: 5px; + height: 5px; + background-color: #FFFFFF; + border-radius: 9999px; + animation: twinkle-stars 0.8s infinite -0.6s; + transition: all 0.4s cubic-bezier(.46,.03,.52,.96); +} + +.welcome-shell .switch__decoration::before, +.welcome-shell .switch__decoration::after { + position: absolute; + display: block; + content: ''; + width: 5px; + height: 5px; + background-color: #FFFFFF; + border-radius: 9999px; +} + +.welcome-shell .switch__decoration::before { + top: -20px; + left: 10px; + opacity: 1; + animation: twinkle-stars 0.6s infinite; +} + +.welcome-shell .switch__decoration::after { + top: -7px; + left: 30px; + animation: twinkle-stars 0.6s infinite -0.2s; +} + +@keyframes twinkle-stars { + 50% { + opacity: 0.2; + } +} + +.welcome-shell .switch__input:checked + .switch__label { + background-color: #8FB5F5; + border-color: #347CF8; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__indicator { + background-color: #ECD21F; + box-shadow: none; + transform: translate(-50%, -50%) translateX(72%); +} + +.welcome-shell .switch__input:checked + .switch__label .switch__indicator::before, +.welcome-shell .switch__input:checked + .switch__label .switch__indicator::after { + display: none; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration { + top: 50%; + transform: translate(0%, -50%); + animation: cloud 8s linear infinite; + width: 20px; + height: 20px; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::before { + width: 10px; + height: 10px; + top: auto; + bottom: 0; + left: -8px; + animation: none; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::after { + width: 15px; + height: 15px; + top: auto; + bottom: 0; + left: 16px; + animation: none; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration, +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::before, +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::after { + border-radius: 9999px 9999px 0 0; +} + +.welcome-shell .switch__input:checked + .switch__label .switch__decoration::after { + border-bottom-right-radius: 9999px; +} + +@keyframes cloud { + 0% { + transform: translate(0%, -50%); + } + 50% { + transform: translate(-50%, -50%); + } + 100% { + transform: translate(0%, -50%); + } +} + +@media (max-width: 768px) { + .welcome-shell .theme-toggle-container { + top: 10px; + right: 10px; + } + + .welcome-shell .switch__label { + width: 100px; + height: 50px; + } + + .welcome-shell .switch__indicator { + width: 33px; + height: 33px; + } +}