Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ const App: React.FC = () => {
const [capturedByWhite, setCapturedByWhite] = useState<Piece[]>([]); // Pieces Black lost to White
const [capturedByBlack, setCapturedByBlack] = useState<Piece[]>([]); // Pieces White lost to Black

// Mobile Overlay State
const [isInfoOverlayVisible, setIsInfoOverlayVisible] = useState(false);

const toggleInfoOverlay = useCallback(() => {
setIsInfoOverlayVisible(prev => !prev);
}, []);

// Helper function to get legal moves (filters out moves that leave king in check)
const getLegalMoves = useCallback((
currentBoard: BoardState,
Expand Down Expand Up @@ -293,13 +300,24 @@ const App: React.FC = () => {

return (
<AppLayout
isInfoOverlayVisible={isInfoOverlayVisible}
toggleInfoOverlay={toggleInfoOverlay}
moveHistory={moveHistory}
gameStatusMessage={message}
onResetGame={resetGame}
isCheckmate={isCheckmate}
isStalemate={isStalemate}
isCheck={isCheck}
capturedByWhiteData={capturedByWhite}
capturedByBlackData={capturedByBlack}
headerContent={
<>
<h1 className="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-sky-400 via-blue-500 to-indigo-600 bg-clip-text text-transparent tracking-tight drop-shadow-lg">
React Chess
</h1>
<div className="mt-3 md:mt-4">
<div className={`inline-flex items-center px-4 py-2 rounded-full text-sm md:text-base font-medium shadow-lg backdrop-blur-sm border transition-all duration-300 ${
{/* This status message display is now only for desktop (lg screens) */}
<div className={`hidden lg:inline-flex items-center px-4 py-2 rounded-full text-sm md:text-base font-medium shadow-lg backdrop-blur-sm border transition-all duration-300 ${
isCheckmate
? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40 shadow-emerald-500/20'
: isStalemate
Expand Down Expand Up @@ -335,7 +353,7 @@ const App: React.FC = () => {
</>
}
mainContent={
<div className="p-4 bg-gradient-to-br from-slate-800/50 to-slate-700/50 backdrop-blur-sm rounded-2xl shadow-2xl border border-slate-600/50">
<div className="p-1 sm:p-2 md:p-4 bg-gradient-to-br from-slate-800/50 to-slate-700/50 backdrop-blur-sm rounded-2xl shadow-2xl border border-slate-600/50">
<BoardComponent
board={board}
selectedSquare={selectedSquare}
Expand Down
104 changes: 86 additions & 18 deletions components/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
import React from 'react';
import { MobileInfoOverlay } from './MobileInfoOverlay'; // Import MobileInfoOverlay
import { CapturedPiecesDisplay } from './CapturedPiecesDisplay'; // Import CapturedPiecesDisplay
import { Piece } from '../types'; // Assuming Piece type might be needed for new props

export interface AppLayoutProps {
headerContent: React.ReactNode;
leftSidebarContent: React.ReactNode;
leftSidebarContent: React.ReactNode; // For desktop
mainContent: React.ReactNode;
rightSidebarContent: React.ReactNode;
rightSidebarContent: React.ReactNode; // For desktop
// Props for MobileInfoOverlay
isInfoOverlayVisible: boolean;
toggleInfoOverlay: () => void;
moveHistory: string[];
gameStatusMessage: string;
onResetGame: () => void;
isCheckmate: boolean;
isStalemate: boolean;
isCheck: boolean;
// Props for mobile captured pieces
capturedByWhiteData: Piece[];
capturedByBlackData: Piece[];
}

export const AppLayout: React.FC<AppLayoutProps> = ({
headerContent,
leftSidebarContent,
mainContent,
rightSidebarContent,
isInfoOverlayVisible,
toggleInfoOverlay,
moveHistory,
gameStatusMessage,
onResetGame,
isCheckmate,
isStalemate,
isCheck,
capturedByWhiteData,
capturedByBlackData,
}) => {
return (
<div className={`
Expand All @@ -28,44 +53,87 @@ export const AppLayout: React.FC<AppLayoutProps> = ({
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl"></div>
</div>

<header className="mb-6 md:mb-8 text-center relative z-10">
{headerContent}
<header className="w-full max-w-sm sm:max-w-md md:max-w-xl lg:max-w-6xl xl:max-w-7xl mb-6 md:mb-8 text-center relative z-10 flex items-center justify-between px-1">
{/* Mobile Info Toggle Button - aligned to the left or start */}
<button
onClick={toggleInfoOverlay}
className="p-2 rounded-md bg-slate-700/80 hover:bg-slate-600/80 text-white lg:hidden shadow-md"
aria-label="Toggle game info panel"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>

{/* Header Content (Title and Desktop Status) - takes remaining space and centers its content */}
<div className="flex-grow">
{headerContent}
</div>

{/* Spacer to balance the mobile button, hidden on lg screens */}
<div className="w-9 h-9 lg:hidden" aria-hidden="true"></div>
</header>

<div className={`
flex flex-col lg:flex-row
gap-6 items-start
gap-2 sm:gap-4 lg:gap-6 items-start
w-full
max-w-sm sm:max-w-md md:max-w-xl lg:max-w-6xl xl:max-w-7xl
relative z-10
`}>
{/* Left Side */}
<div className={`
w-full lg:w-64
order-2 lg:order-1
flex-shrink-0 space-y-4
`}>
{leftSidebarContent}
{/* Mobile: Captured Black Pieces (Top) - order-1 ensures it's first in flex-col */}
<div className="w-full order-1 lg:hidden">
<CapturedPiecesDisplay pieces={capturedByBlackData} title="Black's Captures" />
</div>

{/* Center - Chess Board */}
{/* Main Content (Board) - Order changes for mobile vs desktop */}
{/* On mobile, order-2. On desktop, it's part of lg:flex-row, effectively order-2 by source order among visible lg items */}
<div className={`
flex-grow flex justify-center
order-1 lg:order-2
order-2
w-full lg:w-auto
`}>
{mainContent}
{mainContent} {/* This is the BoardComponent */}
</div>

{/* Mobile: Captured White Pieces (Bottom) - order-3 ensures it's last in flex-col before desktop sidebars */}
<div className="w-full order-3 lg:hidden">
<CapturedPiecesDisplay pieces={capturedByWhiteData} title="White's Captures" />
</div>

{/* Desktop Left Sidebar (contains both captured displays from App.tsx) */}
{/* order-1 for lg screens, hidden on smaller screens */}
<div className={`
w-full lg:w-64
order-1
hidden lg:flex lg:flex-col lg:flex-shrink-0 lg:space-y-4
`}>
{leftSidebarContent}
</div>

{/* Right Side */}
{/* Desktop Right Sidebar (Game Info, Moves, Reset from App.tsx) */}
{/* order-3 for lg screens, hidden on smaller screens */}
<div className={`
w-full lg:w-64
order-3 lg:order-3
flex-shrink-0 space-y-6
order-3
hidden lg:flex lg:flex-col lg:flex-shrink-0 lg:space-y-6
`}>
{rightSidebarContent}
</div>
</div>

{isInfoOverlayVisible && (
<MobileInfoOverlay
isOpen={isInfoOverlayVisible}
onClose={toggleInfoOverlay}
moveHistory={moveHistory}
gameStatusMessage={gameStatusMessage}
onResetGame={onResetGame}
isCheckmate={isCheckmate}
isStalemate={isStalemate}
isCheck={isCheck}
/>
)}
</div>
);
};
14 changes: 7 additions & 7 deletions components/CapturedPiecesDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,31 @@ export interface CapturedPiecesDisplayProps {

export const CapturedPiecesDisplay: React.FC<CapturedPiecesDisplayProps> = ({ pieces, title }) => (
<div className={`
p-4
p-1 sm:p-2 md:p-4
bg-gradient-to-br from-slate-800/80 to-slate-700/80
backdrop-blur-sm rounded-xl shadow-xl
border border-slate-600/50
w-full
`}>
<h3 className={`
text-md font-bold
text-xs sm:text-sm md:text-md font-bold
text-transparent bg-gradient-to-r from-sky-400 to-blue-500 bg-clip-text
mb-3 border-b border-slate-600/50 pb-2
mb-1 sm:mb-2 md:mb-3 border-b border-slate-600/50 pb-1 sm:pb-1 md:pb-2
`}>
{title}
</h3>
<div className={`
flex flex-wrap
gap-x-2 gap-y-1
min-h-[3rem] items-center
gap-x-1 gap-y-0.5 sm:gap-x-2 sm:gap-y-1
min-h-[2rem] sm:min-h-[2.5rem] md:min-h-[3rem] items-center
`}>
{pieces.length === 0 && (
<span className="text-sm text-slate-400 italic">No pieces captured</span>
<span className="text-xs sm:text-sm text-slate-400 italic">No pieces captured</span>
)}
{pieces.map((p, i) => (
<div
key={`${p.type}-${p.color}-${i}`}
className="text-3xl drop-shadow-lg hover:scale-110 transition-transform duration-200"
className="text-lg sm:text-xl md:text-2xl lg:text-3xl drop-shadow-lg hover:scale-110 transition-transform duration-200"
title={`${p.color} ${p.type}`}
>
{/* This usage expects PIECE_UNICODE to be { [color]: { [type]: char } } */}
Expand Down
103 changes: 103 additions & 0 deletions components/MobileInfoOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import NotationDisplay from './NotationDisplay'; // Assuming NotationDisplay is in the same components folder

interface MobileInfoOverlayProps {
isOpen: boolean;
onClose: () => void;
moveHistory: string[];
gameStatusMessage: string;
onResetGame: () => void;
isCheckmate: boolean; // Added to determine button text
isStalemate: boolean; // Added to determine button text
isCheck: boolean; // Added for status message styling
}

export const MobileInfoOverlay: React.FC<MobileInfoOverlayProps> = ({
isOpen,
onClose,
moveHistory,
gameStatusMessage,
onResetGame,
isCheckmate,
isStalemate,
isCheck,
}) => {
if (!isOpen) {
return null;
}

// Determine status message styling based on game state
// Similar to App.tsx, but adapted for the overlay
let statusMessageClasses = 'px-3 py-2 rounded-md text-sm font-medium text-center ';
if (isCheckmate) {
statusMessageClasses += 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40';
} else if (isStalemate) {
statusMessageClasses += 'bg-yellow-500/20 text-yellow-300 border border-yellow-500/40';
} else if (isCheck) {
statusMessageClasses += 'bg-red-500/20 text-red-300 border border-red-500/40 animate-pulse';
} else {
statusMessageClasses += 'bg-slate-700/60 text-slate-300 border border-slate-600/40';
}


return (
<div
className="fixed inset-0 z-40 bg-slate-900/70 backdrop-blur-sm lg:hidden"
onClick={onClose} // Close when clicking backdrop
role="dialog"
aria-modal="true"
>
<div
className="fixed inset-x-4 bottom-4 top-16 z-50 flex flex-col gap-4 p-4 bg-slate-800 rounded-lg shadow-xl lg:hidden overflow-y-auto scrollbar-thin scrollbar-thumb-slate-600 scrollbar-track-slate-700"
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside panel
>
{/* Close Button */}
<button
onClick={onClose}
className="absolute top-2 right-2 p-2 text-slate-400 hover:text-slate-200 transition-colors"
aria-label="Close info panel"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>

{/* Game Status Message */}
<div className={statusMessageClasses}>
{gameStatusMessage}
</div>

{/* Move History */}
<div className="flex-grow flex flex-col min-h-0"> {/* Ensure NotationDisplay can shrink and scroll */}
<NotationDisplay moveHistory={moveHistory} />
</div>

{/* Reset Game Button */}
<button
type="button"
onClick={() => {
onResetGame();
onClose(); // Close overlay after resetting
}}
className={`
w-full px-6 py-3 mt-auto
bg-gradient-to-r from-sky-600 to-blue-600
hover:from-sky-500 hover:to-blue-500
text-white font-bold text-base rounded-xl
shadow-xl transform transition-all duration-200
hover:scale-105 hover:shadow-2xl
focus:outline-none focus:ring-4 focus:ring-sky-500 focus:ring-opacity-50
active:scale-95
`}
>
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{isCheckmate || isStalemate ? 'Play Again' : 'Reset Game'}
</span>
</button>
</div>
</div>
);
};
Loading