From 037d88d44adb31ea62e054d218f07aebf8d69c21 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:53:27 +0000 Subject: [PATCH] Changes --- src/hooks/useSwipeControls.ts | 107 ++++++++++++++++++++++++++++++++++ src/pages/AIChat.tsx | 4 +- src/pages/Pacman.tsx | 38 +++++++----- src/pages/Snake.tsx | 33 +++++++---- src/pages/Tetris.tsx | 59 ++++++++++++++----- 5 files changed, 198 insertions(+), 43 deletions(-) create mode 100644 src/hooks/useSwipeControls.ts diff --git a/src/hooks/useSwipeControls.ts b/src/hooks/useSwipeControls.ts new file mode 100644 index 0000000..c76c845 --- /dev/null +++ b/src/hooks/useSwipeControls.ts @@ -0,0 +1,107 @@ +import { useRef, useCallback, useEffect } from 'react'; + +export type SwipeDirection = 'up' | 'down' | 'left' | 'right'; + +interface SwipeState { + startX: number; + startY: number; + startTime: number; +} + +interface UseSwipeControlsOptions { + onSwipe: (direction: SwipeDirection, isFastSwipe: boolean) => void; + onTap?: () => void; + minSwipeDistance?: number; + fastSwipeThreshold?: number; // pixels per millisecond + enabled?: boolean; +} + +/** + * Hook for detecting swipe gestures on touch devices + * Supports directional swipes and fast swipe detection (for hard drop in Tetris) + */ +export const useSwipeControls = ({ + onSwipe, + onTap, + minSwipeDistance = 30, + fastSwipeThreshold = 1.5, + enabled = true, +}: UseSwipeControlsOptions) => { + const swipeStateRef = useRef(null); + + const handleTouchStart = useCallback((e: TouchEvent) => { + if (!enabled) return; + const touch = e.touches[0]; + swipeStateRef.current = { + startX: touch.clientX, + startY: touch.clientY, + startTime: Date.now(), + }; + }, [enabled]); + + const handleTouchEnd = useCallback((e: TouchEvent) => { + if (!enabled || !swipeStateRef.current) return; + + const touch = e.changedTouches[0]; + const { startX, startY, startTime } = swipeStateRef.current; + + const deltaX = touch.clientX - startX; + const deltaY = touch.clientY - startY; + const deltaTime = Date.now() - startTime; + + const absX = Math.abs(deltaX); + const absY = Math.abs(deltaY); + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + const speed = distance / Math.max(deltaTime, 1); + + // If movement is too small, treat as tap + if (distance < minSwipeDistance) { + onTap?.(); + swipeStateRef.current = null; + return; + } + + const isFastSwipe = speed > fastSwipeThreshold; + + // Determine direction based on larger axis + let direction: SwipeDirection; + if (absX > absY) { + direction = deltaX > 0 ? 'right' : 'left'; + } else { + direction = deltaY > 0 ? 'down' : 'up'; + } + + onSwipe(direction, isFastSwipe); + swipeStateRef.current = null; + }, [enabled, minSwipeDistance, fastSwipeThreshold, onSwipe, onTap]); + + const handleTouchCancel = useCallback(() => { + swipeStateRef.current = null; + }, []); + + // Returns handlers to attach to an element + const getSwipeHandlers = useCallback(() => ({ + onTouchStart: (e: React.TouchEvent) => handleTouchStart(e.nativeEvent), + onTouchEnd: (e: React.TouchEvent) => handleTouchEnd(e.nativeEvent), + onTouchCancel: () => handleTouchCancel(), + }), [handleTouchStart, handleTouchEnd, handleTouchCancel]); + + // Alternative: attach to a specific element ref + const bindToElement = useCallback((element: HTMLElement | null) => { + if (!element) return; + + element.addEventListener('touchstart', handleTouchStart, { passive: true }); + element.addEventListener('touchend', handleTouchEnd, { passive: true }); + element.addEventListener('touchcancel', handleTouchCancel, { passive: true }); + + return () => { + element.removeEventListener('touchstart', handleTouchStart); + element.removeEventListener('touchend', handleTouchEnd); + element.removeEventListener('touchcancel', handleTouchCancel); + }; + }, [handleTouchStart, handleTouchEnd, handleTouchCancel]); + + return { getSwipeHandlers, bindToElement }; +}; + +export default useSwipeControls; diff --git a/src/pages/AIChat.tsx b/src/pages/AIChat.tsx index 10028d6..0822ebb 100644 --- a/src/pages/AIChat.tsx +++ b/src/pages/AIChat.tsx @@ -389,7 +389,7 @@ const AIChat = () => { transition={{ duration: 0.5 }} className={`flex flex-col ${ isFullscreen - ? 'fixed inset-0 z-50 bg-background p-3 md:p-8 overflow-hidden' + ? 'fixed inset-0 z-50 bg-background p-2 sm:p-3 md:p-8 overflow-hidden w-screen h-screen' : 'h-full' }`} > @@ -559,7 +559,7 @@ const AIChat = () => { )}
{ const [cellSize, setCellSize] = useState(getCellSize); const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; + // Swipe controls for mobile + const { getSwipeHandlers } = useSwipeControls({ + onSwipe: (dir) => { + if (gameMode === '1p' && gameStarted && !gameOver && !gameComplete && !isPaused) { + setNextDirection(dir as Direction); + } + }, + onTap: () => { + if (gameStarted && !gameOver && !gameComplete) setIsPaused(p => !p); + }, + enabled: isMobile && gameStarted && !gameOver && !gameComplete, + }); + useEffect(() => { const handleResize = () => setCellSize(getCellSize()); window.addEventListener('resize', handleResize); @@ -731,21 +744,18 @@ const Pacman = () => { {showModeSelection ? (
-

SELECT MODE

- - +

MOBILE: 1 PLAYER ONLY

+
) : gameStarted && !gameOver && !gameComplete ? ( -
-
- setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑ -
- setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">← - - setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→ -
- setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓ -
+
+

+ SWIPE TO MOVE
+ TAP TO PAUSE +

) : (
{gameStarted && !gameOver && !gameComplete ? ( -
-
- directionQueueRef.current.push('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑ -
- directionQueueRef.current.push('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">← - - directionQueueRef.current.push('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→ -
- directionQueueRef.current.push('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓ -
+
+

+ SWIPE TO MOVE
+ TAP TO PAUSE +

) : (
{gameStarted && !gameOver ? ( -
- {/* D-pad style controls */} -
-
- -
- - - -
- -
-
- {/* Pause button on the side */} - +
+

+ ← → SWIPE TO MOVE
+ ↓ SOFT DROP · FAST ↓ HARD DROP
+ ↑ ROTATE · TAP TO PAUSE +

) : (