Compare commits

...

4 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
d09cda3510 Fix mobile UI issues
Revert and stabilize mobile rendering for games and sidebar:
- Remove problematic auto-collapse effect on route changes
- Adjust mobile canvas and control sizing across Breakout, Tetris, Snake, Pacman, and MatrixRain to avoid content clipping
- Improve matrix rain initialization timing on mobile
- Add tweaks to ensure sidebar behavior is reliable on mobile navigation

X-Lovable-Edit-ID: edt-353f2bea-4ee7-4f41-b9e9-f6f1090dcc9e
2025-12-05 20:06:35 +00:00
gpt-engineer-app[bot]
225b337a27 Changes 2025-12-05 20:06:34 +00:00
gpt-engineer-app[bot]
6ff0bbb8ef Improve mobile UI fixes
Addressed mobile-specific issues:
- Stabilized Matrix Rain startup on phones
- Adjusted mobile game layout and controls to render correctly
- Auto-collapse mobile sidebar when navigating between routes

X-Lovable-Edit-ID: edt-f21d53ab-1980-44e2-b1ea-ac60805a8999
2025-12-05 19:29:02 +00:00
gpt-engineer-app[bot]
1d011db6f8 Changes 2025-12-05 19:29:02 +00:00
5 changed files with 58 additions and 41 deletions

View File

@ -22,6 +22,7 @@ const MatrixRain = ({ color = '#00FF00' }: MatrixRainProps) => {
const fontSize = 16; const fontSize = 16;
let columns: number; let columns: number;
let rainDrops: number[] = []; let rainDrops: number[] = [];
let animationStarted = false;
const katakana = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッン'; const katakana = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレヱゲゼデベペオォコソトノホモヨョロヲゴゾドボポヴッン';
const latin = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const latin = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
@ -29,18 +30,23 @@ const MatrixRain = ({ color = '#00FF00' }: MatrixRainProps) => {
const alphabet = katakana + latin + nums; const alphabet = katakana + latin + nums;
const init = () => { const init = () => {
canvas.width = window.innerWidth; // Use document dimensions for reliability
canvas.height = window.innerHeight; const width = document.documentElement.clientWidth || window.innerWidth;
const height = document.documentElement.clientHeight || window.innerHeight;
canvas.width = width;
canvas.height = height;
columns = Math.floor(canvas.width / fontSize); columns = Math.floor(canvas.width / fontSize);
rainDrops = []; rainDrops = [];
for (let x = 0; x < columns; x++) { for (let x = 0; x < columns; x++) {
rainDrops[x] = Math.random() * canvas.height / fontSize; rainDrops[x] = Math.random() * canvas.height / fontSize;
} }
animationStarted = true;
}; };
init();
const draw = () => { const draw = () => {
if (!animationStarted) return;
ctx.fillStyle = 'rgba(0, 0, 0, 0.04)'; ctx.fillStyle = 'rgba(0, 0, 0, 0.04)';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
@ -58,12 +64,19 @@ const MatrixRain = ({ color = '#00FF00' }: MatrixRainProps) => {
} }
}; };
// Initialize after a brief delay to ensure DOM is ready
const initTimeout = setTimeout(init, 50);
const interval = setInterval(draw, 30); const interval = setInterval(draw, 30);
const handleResize = () => init(); const handleResize = () => {
init();
};
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', () => setTimeout(init, 150));
return () => { return () => {
clearTimeout(initTimeout);
clearInterval(interval); clearInterval(interval);
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
}; };

View File

@ -51,9 +51,10 @@ const Breakout = () => {
const isMobile = window.innerWidth < 768; const isMobile = window.innerWidth < 768;
if (isMobile) { if (isMobile) {
const maxWidth = window.innerWidth - 32; const maxWidth = window.innerWidth - 32;
const maxHeight = window.innerHeight - 280; // Reserve space for: header(50) + stats(40) + controls(80) + margins(30) = 200
const maxHeight = window.innerHeight - 200;
const aspectRatio = 480 / 580; const aspectRatio = 480 / 580;
let width = maxWidth; let width = Math.min(maxWidth, 400);
let height = width / aspectRatio; let height = width / aspectRatio;
if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; } if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; }
return { width: Math.floor(width), height: Math.floor(height) }; return { width: Math.floor(width), height: Math.floor(height) };
@ -269,7 +270,7 @@ const Breakout = () => {
)} )}
{isMobile && ( {isMobile && (
<div className="mt-4 flex flex-col items-center gap-2 w-full"> <div className="mt-2 flex flex-col items-center gap-1 w-full flex-shrink-0">
<div className="flex gap-4 text-center"> <div className="flex gap-4 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
@ -277,10 +278,10 @@ const Breakout = () => {
<div><p className="font-pixel text-[8px] text-foreground/60"></p><p className="font-minecraft text-sm text-primary">{lives}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60"></p><p className="font-minecraft text-sm text-primary">{lives}</p></div>
</div> </div>
{gameStarted && !gameOver && ( {gameStarted && !gameOver && (
<div className="flex gap-4 mt-2"> <div className="flex gap-4 mt-1">
<GameTouchButton onAction={moveLeft} className="p-4 px-8 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}></GameTouchButton> <GameTouchButton onAction={moveLeft} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}></GameTouchButton>
<button onClick={() => setIsPaused(p => !p)} className="p-4 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button> <button onClick={() => setIsPaused(p => !p)} className="p-3 px-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
<GameTouchButton onAction={moveRight} className="p-4 px-8 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}></GameTouchButton> <GameTouchButton onAction={moveRight} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}></GameTouchButton>
</div> </div>
)} )}
{(!gameStarted || gameOver) && ( {(!gameStarted || gameOver) && (

View File

@ -73,8 +73,9 @@ const Pacman = () => {
if (typeof window === 'undefined') return 20; if (typeof window === 'undefined') return 20;
const isMobile = window.innerWidth < 768; const isMobile = window.innerWidth < 768;
if (isMobile) { if (isMobile) {
const maxWidth = window.innerWidth - 40; const maxWidth = window.innerWidth - 32;
const maxHeight = window.innerHeight - 300; // Reserve space for: header(50) + stats(40) + controls(120) + margins(30) = 240
const maxHeight = window.innerHeight - 240;
return Math.min(Math.floor(maxWidth / GRID_WIDTH), Math.floor(maxHeight / GRID_HEIGHT), 16); return Math.min(Math.floor(maxWidth / GRID_WIDTH), Math.floor(maxHeight / GRID_HEIGHT), 16);
} }
return isFullscreen ? 26 : 20; return isFullscreen ? 26 : 20;
@ -342,22 +343,22 @@ const Pacman = () => {
)} )}
{isMobile && ( {isMobile && (
<div className="mt-4 flex flex-col items-center gap-2 w-full"> <div className="mt-2 flex flex-col items-center gap-1 w-full flex-shrink-0">
<div className="flex gap-4 text-center"> <div className="flex gap-4 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
</div> </div>
{gameStarted && !gameOver && !gameComplete && ( {gameStarted && !gameOver && !gameComplete && (
<div className="grid grid-cols-3 gap-1 mt-2"> <div className="grid grid-cols-3 gap-1 mt-1">
<div /> <div />
<GameTouchButton onAction={() => setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => setNextDirection('up')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<div /> <div />
<GameTouchButton onAction={() => setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => setNextDirection('left')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button> <button onClick={() => setIsPaused(p => !p)} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
<GameTouchButton onAction={() => setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => setNextDirection('right')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<div /> <div />
<GameTouchButton onAction={() => setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => setNextDirection('down')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<div /> <div />
</div> </div>
)} )}

View File

@ -39,8 +39,9 @@ const Snake = () => {
if (typeof window === 'undefined') return 24; if (typeof window === 'undefined') return 24;
const isMobile = window.innerWidth < 768; const isMobile = window.innerWidth < 768;
if (isMobile) { if (isMobile) {
const maxWidth = window.innerWidth - 40; const maxWidth = window.innerWidth - 32;
const maxHeight = window.innerHeight - 300; // Reserve space for: header(50) + stats(40) + controls(120) + margins(30) = 240
const maxHeight = window.innerHeight - 240;
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 18); return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 18);
} }
return isFullscreen ? 28 : 24; return isFullscreen ? 28 : 24;
@ -303,22 +304,22 @@ const Snake = () => {
{/* Mobile Controls */} {/* Mobile Controls */}
{isMobile && ( {isMobile && (
<div className="mt-4 flex flex-col items-center gap-2 w-full"> <div className="mt-2 flex flex-col items-center gap-1 w-full flex-shrink-0">
<div className="flex gap-4 text-center"> <div className="flex gap-4 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LEN</p><p className="font-minecraft text-sm text-primary">{snake.length}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">LEN</p><p className="font-minecraft text-sm text-primary">{snake.length}</p></div>
</div> </div>
{gameStarted && !gameOver && !gameComplete && ( {gameStarted && !gameOver && !gameComplete && (
<div className="grid grid-cols-3 gap-1 mt-2"> <div className="grid grid-cols-3 gap-1 mt-1">
<div /> <div />
<GameTouchButton onAction={() => directionQueueRef.current.push('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => directionQueueRef.current.push('up')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<div /> <div />
<GameTouchButton onAction={() => directionQueueRef.current.push('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => directionQueueRef.current.push('left')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button> <button onClick={() => setIsPaused(p => !p)} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
<GameTouchButton onAction={() => directionQueueRef.current.push('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => directionQueueRef.current.push('right')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<div /> <div />
<GameTouchButton onAction={() => directionQueueRef.current.push('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={() => directionQueueRef.current.push('down')} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<div /> <div />
</div> </div>
)} )}

View File

@ -76,9 +76,10 @@ const Tetris = () => {
if (typeof window === 'undefined') return 24; if (typeof window === 'undefined') return 24;
const isMobile = window.innerWidth < 768; const isMobile = window.innerWidth < 768;
if (isMobile) { if (isMobile) {
const maxWidth = window.innerWidth - 40; const maxWidth = window.innerWidth - 32;
const maxHeight = window.innerHeight - 320; // Reserve space for: header(50) + stats(40) + controls(120) + margins(30) = 240
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 22); const maxHeight = window.innerHeight - 240;
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 20);
} }
return isFullscreen ? 30 : 24; return isFullscreen ? 30 : 24;
}, [isFullscreen]); }, [isFullscreen]);
@ -316,22 +317,22 @@ const Tetris = () => {
</div> </div>
)} )}
{isMobile && ( {isMobile && (
<div className="mt-4 flex flex-col items-center gap-2 w-full"> <div className="mt-2 flex flex-col items-center gap-1 w-full flex-shrink-0">
<div className="flex gap-4 text-center"> <div className="flex gap-4 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{lines}</p></div> <div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{lines}</p></div>
</div> </div>
{gameStarted && !gameOver && !gameComplete && ( {gameStarted && !gameOver && !gameComplete && (
<div className="grid grid-cols-3 gap-1 mt-2"> <div className="grid grid-cols-3 gap-1 mt-1">
<div /> <div />
<GameTouchButton onAction={rotatePiece} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={200}></GameTouchButton> <GameTouchButton onAction={rotatePiece} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={200}></GameTouchButton>
<div /> <div />
<GameTouchButton onAction={moveLeft} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={moveLeft} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<GameTouchButton onAction={hardDrop} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px]" interval={500}>DROP</GameTouchButton> <GameTouchButton onAction={hardDrop} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px]" interval={500}>DROP</GameTouchButton>
<GameTouchButton onAction={moveRight} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={moveRight} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<button onClick={togglePause} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button> <button onClick={togglePause} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
<GameTouchButton onAction={moveDown} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton> <GameTouchButton onAction={moveDown} className="p-3 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg"></GameTouchButton>
<div /> <div />
</div> </div>
)} )}