https://checkmate.social
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

better resign button

jcalabro 3f596c2a 9a5356ba

+56 -34
+42 -33
client/src/components/game/GameScreen.tsx
··· 4 4 * Layout: 5 5 * - Opponent player bar (top) with clock 6 6 * - Chess board (center) 7 - * - Your player bar (bottom) with clock 7 + * - Your player bar (bottom) with clock + inline Resign button 8 8 * - Move list sidebar (right, collapses below on mobile) 9 9 * - Game status overlay (when game ends) 10 - * - Resign button (multiplayer only) 11 10 * 12 11 * In solo mode (both sides are the same player), the board always shows 13 12 * white's perspective and both sides are always movable. Solo games are ··· 176 175 /> 177 176 ) : undefined; 178 177 178 + // --------------------------------------------------------------------------- 179 + // Resign control rendered inline on the local player's bar. 180 + // 181 + // Inlining keeps it always visible (the player bar is never below the fold 182 + // even on short viewports) and groups the action with the player it acts 183 + // upon. Always rendered while the game is active — in solo mode, it just 184 + // ends the practice game and returns to the lobby (the server's resign 185 + // reducer happens to record "black" as the winner, but GameStatus suppresses 186 + // win/loss framing for solo games). 187 + // --------------------------------------------------------------------------- 188 + const resignAction = activeGame.status === 'active' ? ( 189 + showResignConfirm ? ( 190 + <div className="flex items-center gap-1.5"> 191 + <span className="hidden text-xs font-medium text-wood-300 sm:inline">Resign?</span> 192 + <button 193 + onClick={handleResign} 194 + className="rounded-md bg-red-700 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-white shadow-sm transition-colors hover:bg-red-600" 195 + aria-label="Confirm resign" 196 + > 197 + Yes 198 + </button> 199 + <button 200 + onClick={() => setShowResignConfirm(false)} 201 + className="rounded-md border border-wood-500 bg-wood-800 px-2.5 py-1 text-xs font-medium text-wood-200 transition-colors hover:border-wood-300 hover:text-wood-100" 202 + aria-label="Cancel resign" 203 + > 204 + No 205 + </button> 206 + </div> 207 + ) : ( 208 + <button 209 + onClick={() => setShowResignConfirm(true)} 210 + className="rounded-md border border-red-900/70 bg-wood-900 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-red-300 shadow-sm transition-colors hover:border-red-600 hover:bg-red-900/50 hover:text-red-100" 211 + > 212 + Resign 213 + </button> 214 + ) 215 + ) : undefined; 216 + 179 217 return ( 180 218 <div className="flex flex-1 flex-col items-center justify-center gap-4 p-4 lg:flex-row"> 181 219 {/* Board + player bars column */} ··· 211 249 /> 212 250 </div> 213 251 214 - {/* You (bottom) */} 252 + {/* You (bottom) — Resign button rendered inline via the action slot */} 215 253 <PlayerBar 216 254 displayName={isSolo ? (bottomColor === 'white' ? 'White' : 'Black') : (displayName ?? 'You')} 217 255 handle={isSolo ? undefined : (handle ?? undefined)} ··· 219 257 isActive={bottomIsActive} 220 258 color={bottomColor} 221 259 clock={bottomClock} 260 + action={resignAction} 222 261 /> 223 - 224 - {/* Resign button — multiplayer only; resigning against yourself makes no sense */} 225 - {activeGame.status === 'active' && !isSolo && ( 226 - <div className="mt-1 flex justify-center"> 227 - {showResignConfirm ? ( 228 - <div className="flex items-center gap-2"> 229 - <span className="text-sm text-wood-400">Resign?</span> 230 - <button 231 - onClick={handleResign} 232 - className="rounded px-3 py-1 text-sm bg-red-700 text-white transition-colors hover:bg-red-800" 233 - > 234 - Yes 235 - </button> 236 - <button 237 - onClick={() => setShowResignConfirm(false)} 238 - className="rounded border border-wood-600 px-3 py-1 text-sm text-wood-300 transition-colors hover:border-wood-500 hover:text-wood-100" 239 - > 240 - No 241 - </button> 242 - </div> 243 - ) : ( 244 - <button 245 - onClick={() => setShowResignConfirm(true)} 246 - className="rounded px-4 py-1.5 text-sm text-wood-500 transition-colors hover:bg-wood-700 hover:text-wood-300" 247 - > 248 - Resign 249 - </button> 250 - )} 251 - </div> 252 - )} 253 262 </div> 254 263 255 264 {/* Move list sidebar */}
+5
client/src/components/game/GameStatus.tsx
··· 42 42 title = 'Draw'; 43 43 subtitle = 'The game ended in a draw.'; 44 44 break; 45 + case 'resigned': 46 + emoji = '🏳'; 47 + title = 'Game ended'; 48 + subtitle = 'You ended the practice game.'; 49 + break; 45 50 default: 46 51 title = 'Game over'; 47 52 subtitle = '';
+9 -1
client/src/components/game/PlayerBar.tsx
··· 2 2 * PlayerBar — displays player info above/below the chess board. 3 3 * 4 4 * Shows avatar, display name, handle, a turn indicator, and (for timed 5 - * games) the player's remaining clock time. 5 + * games) the player's remaining clock time. May optionally render a 6 + * trailing `action` slot (e.g., the Resign button on the local player's 7 + * bar) — kept generic so PlayerBar stays unaware of game-level controls. 6 8 */ 7 9 8 10 import type { ReactNode } from 'react'; ··· 15 17 color: 'white' | 'black'; 16 18 /** Optional clock node rendered on the right side of the bar */ 17 19 clock?: ReactNode; 20 + /** Optional action node rendered at the far right (e.g. Resign button) */ 21 + action?: ReactNode; 18 22 } 19 23 20 24 export function PlayerBar({ ··· 24 28 isActive, 25 29 color, 26 30 clock, 31 + action, 27 32 }: PlayerBarProps) { 28 33 return ( 29 34 <div ··· 68 73 {isActive && ( 69 74 <div className="h-2 w-2 animate-pulse rounded-full bg-felt-400" /> 70 75 )} 76 + 77 + {/* Optional trailing action (e.g. Resign) */} 78 + {action} 71 79 </div> 72 80 ); 73 81 }