diff --git a/backend/internal/game/chess.go b/backend/internal/game/chess.go index 11978f2..ed5d279 100644 --- a/backend/internal/game/chess.go +++ b/backend/internal/game/chess.go @@ -1,7 +1,8 @@ package game import ( - "fmt" + "errors" + "log/slog" "time" "github.com/corentings/chess/v2" @@ -71,17 +72,24 @@ func (c *chessGame) Move(sender string, mv *GameMove) error { return err } - moveStr := rowCol2Move(mv.From) + rowCol2Move(mv.To) - if mv.Change != "" { - moveStr += mv.Change + from := chess.Square((7-mv.From.Row)*8 + mv.From.Col) + to := chess.Square((7-mv.To.Row)*8 + mv.To.Col) + promo := change2Piece(mv.Change) + + var move *chess.Move + for _, m := range c.game.ValidMoves() { + if m.S1() == from && m.S2() == to && m.Promo() == promo { + move = &m + break + } } - notation := chess.UCINotation{} - move, err := notation.Decode(c.game.Position(), moveStr) - if err != nil { - return fmt.Errorf("invalid move format %q: %w", moveStr, err) + if move == nil { + return errors.New("invalid move: not legal in this position") } - if c.game.Move(move, nil) != nil { - return fmt.Errorf("invalid move %q", moveStr) + + if err := c.game.Move(move, nil); err != nil { + slog.Error("Internal error", "error", err) + return errors.New("internal error") } if c.game.Outcome() != chess.NoOutcome { @@ -89,9 +97,13 @@ func (c *chessGame) Move(sender string, mv *GameMove) error { c.endedAt = time.Now() switch c.game.Outcome() { case chess.WhiteWon: + c.status = StatusFin c.winner = c.players[0] case chess.BlackWon: + c.status = StatusFin c.winner = c.players[1] + case chess.Draw: + c.status = StatusFin } } c.turn = 1 - c.turn @@ -102,8 +114,18 @@ func (c *chessGame) Move(sender string, mv *GameMove) error { return nil } -func rowCol2Move(pos Position) string { - return fmt.Sprintf("%c%d", 'a'+pos.Col, 8-pos.Row) +func change2Piece(change string) chess.PieceType { + switch change { + case "q", "Q": + return chess.Queen + case "r", "R": + return chess.Rook + case "b", "B": + return chess.Bishop + case "n", "N": + return chess.Knight + } + return chess.NoPieceType } func pieceToCode(piece chess.Piece) int { diff --git a/backend/internal/game/game.go b/backend/internal/game/game.go index a44c98e..81bced1 100644 --- a/backend/internal/game/game.go +++ b/backend/internal/game/game.go @@ -47,7 +47,7 @@ type Game interface { getBoardLocked() any getValidMovesLocked() []GameMove updateLoop() - handleDisconnects() + handleDisconnectLocked() } type GameUpdate struct { @@ -158,20 +158,7 @@ func (b *baseGame) Leave(player string, intentional bool) { b.players = slices.Delete(b.players, idx, idx+1) } - if len(b.players) == 0 { - b.status = StatusFin - b.endedAt = time.Now() - b.notify(GameUpdate{ - State: b.stateLocked(), - Action: DeleteAction, - }) - b.Stop() - return - } - b.notify(GameUpdate{ - State: b.stateLocked(), - Action: UpdateAction, - }) + b.handleDisconnectLocked() } func (b *baseGame) checkTurnLocked(sender string) (int, error) { @@ -260,11 +247,11 @@ func (b *baseGame) updateLoop() { b.players = slices.Delete(b.players, idx, idx+1) } } - b.self.handleDisconnects() + b.self.handleDisconnectLocked() } } -func (b *baseGame) handleDisconnects() { +func (b *baseGame) handleDisconnectLocked() { // just stop game if player doesn't come back. b.status = StatusFin b.endedAt = time.Now() diff --git a/backend/internal/live/client.go b/backend/internal/live/client.go index 31879fc..a1b5b3c 100644 --- a/backend/internal/live/client.go +++ b/backend/internal/live/client.go @@ -50,14 +50,15 @@ func (c *client) start() { func (c *client) stop() { c.cancel() - c.hub.unregister <- c close(c.send) close(c.recv) c.conn.Close(websocket.StatusNormalClosure, "client leaving") } func (c *client) readPump() { - defer c.stop() + defer func() { + c.hub.unregister <- c + }() for { msgType, msgRaw, err := c.conn.Read(c.ctx) @@ -99,11 +100,18 @@ func (c *client) writePump() { if !ok { return } + writeCtx, cancelWrite := context.WithTimeout(c.ctx, c.cfg.WriteTimeout) err := c.conn.Write(writeCtx, websocket.MessageText, message) cancelWrite() + if err != nil { - slog.Error("writePump: WebSocket write error", "error", err, "client", c.ID) + if websocket.CloseStatus(err) == websocket.StatusNormalClosure || + websocket.CloseStatus(err) == websocket.StatusGoingAway { + slog.Debug("WebSocket connection closed", "client", c.ID) + } else { + slog.Error("writePump: WebSocket write error", "error", err, "client", c.ID) + } c.cancel() return } @@ -179,6 +187,12 @@ func (c *client) processPump() { } func (c *client) trySend(msg []byte) { + defer func() { + if r := recover(); r != nil { + slog.Warn("trySend: Attempted to send on closed channel", "client", c.ID, "recover", r) + } + }() + select { case c.send <- msg: default: diff --git a/backend/internal/live/hub.go b/backend/internal/live/hub.go index eb65210..e2ba83c 100644 --- a/backend/internal/live/hub.go +++ b/backend/internal/live/hub.go @@ -4,6 +4,7 @@ import ( "letsgo/internal/config" "letsgo/internal/game" "log/slog" + "time" ) type hub struct { @@ -52,6 +53,7 @@ func (h *hub) run() { delete(h.clients, client) slog.Debug("Unregistered: ", "client", client.ID) } + time.AfterFunc(100*time.Millisecond, client.stop) case pair := <-h.joinRoom: slog.Debug("join room") diff --git a/frontend/src/app/live/boardgame/page.tsx b/frontend/src/app/live/boardgame/page.tsx index e16ece6..77e1527 100644 --- a/frontend/src/app/live/boardgame/page.tsx +++ b/frontend/src/app/live/boardgame/page.tsx @@ -7,8 +7,8 @@ import { ChessBoard } from '@/components/games/chess/ChessBoard'; import { GameStatus } from '@/components/games/GameStatus'; import { CreateGame } from '@/components/games/CreateGame'; import { GameBoardProps } from '@/types/gameTypes'; -import useBoardGame from '@/hooks/useBoardGame'; -import { useWebSocket } from '@/hooks/webSocket'; +import useBoardGame from '@/hooks/useWSGame'; +import { useWebSocket } from '@/hooks/useWebsocket'; import { GAME_DISPLAY_NAMES } from '@/config/consts'; import { Trophy } from 'lucide-react'; @@ -36,7 +36,7 @@ export default function BoardGamePage() { {(gameState.status === 'finished') &&
- {gameState.winner ?? 'Nobody'} Wins! + {gameState.winner == '' ? 'Nobody' : gameState.winner} Wins!
} @@ -56,7 +56,7 @@ export default function BoardGamePage() { leaveGame={leaveGame} /> -
+
{gameState.gameName !== '' ? GameBoard ?
diff --git a/frontend/src/app/live/draw/page.tsx b/frontend/src/app/live/draw/page.tsx index 15a2fd1..1fc8079 100644 --- a/frontend/src/app/live/draw/page.tsx +++ b/frontend/src/app/live/draw/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useDraw } from '@/hooks/useDraw'; -import { useWebSocket } from '@/hooks/webSocket'; +import { useWebSocket } from '@/hooks/useWebsocket'; import { DrawToolbar } from '@/components/draw/DrawToolbar'; import { useState, useRef, useEffect } from 'react'; import { DRAW_CANVAS_WIDTH, DRAW_CANVAS_HEIGHT } from '@/config/consts'; @@ -65,7 +65,6 @@ export default function DrawPage() { return () => window.removeEventListener('resize', updateZoom); }, [containerRef]); - const getCanvasCoords = (clientX: number, clientY: number) => { if (!containerRef.current) return { x: 0, y: 0 }; const rect = containerRef.current.getBoundingClientRect(); @@ -75,6 +74,24 @@ export default function DrawPage() { }; }; + const handleZoom = (zoom: number) => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const centerX = rect.width / 2; + const centerY = rect.height / 2; + + const x = ((centerX / containerZoom) - viewState.offset.x) / viewState.zoom; + const y = ((centerY / containerZoom) - viewState.offset.y) / viewState.zoom; + + setViewState({ + zoom: zoom, + offset: { + x: (centerX / containerZoom) - x * zoom, + y: (centerY / containerZoom) - y * zoom, + }, + }); + } + const startPan = (clientX: number, clientY: number) => { setIsPanning(true); panStartRef.current = { @@ -88,7 +105,7 @@ export default function DrawPage() { const handlePan = (clientX: number, clientY: number) => { if (!isPanning || !panStartRef.current) return; - const scale = 1 / (containerZoom * viewState.zoom); + const scale = 1 / (containerZoom); const dx = (clientX - panStartRef.current.x) * scale; const dy = (clientY - panStartRef.current.y) * scale; @@ -197,14 +214,14 @@ export default function DrawPage() { currentZoom={viewState.zoom} changeColor={color => setDrawState(s => ({ ...s, color }))} changeWidth={lineWidth => setDrawState(s => ({ ...s, lineWidth }))} - zoomIn={() => setViewState(s => ({ ...s, zoom: Math.min(s.zoom * 1.1, 3) }))} - zoomOut={() => setViewState(s => ({ ...s, zoom: Math.max(s.zoom * 0.9, 1) }))} + zoomIn={() => handleZoom(Math.min(viewState.zoom * 1.1, 3))} + zoomOut={() => handleZoom(Math.max(viewState.zoom * 0.9, 1))} zoomReset={() => setViewState({ zoom: 1, offset: { x: 0, y: 0 } })} />
- +
; } diff --git a/frontend/src/components/UI/Dropdown.tsx b/frontend/src/components/UI/Dropdown.tsx index edae9a4..451617d 100644 --- a/frontend/src/components/UI/Dropdown.tsx +++ b/frontend/src/components/UI/Dropdown.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; import Link from 'next/link'; import Button from './Button'; diff --git a/frontend/src/components/UI/NavBar.tsx b/frontend/src/components/UI/NavBar.tsx index 3a6c64d..6bd0a28 100644 --- a/frontend/src/components/UI/NavBar.tsx +++ b/frontend/src/components/UI/NavBar.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import Link from 'next/link'; import Dropdown from './Dropdown'; import ThemeSwitcher from './ThemeSwitcher'; diff --git a/frontend/src/components/UI/ThemeSwitcher.tsx b/frontend/src/components/UI/ThemeSwitcher.tsx index 5eacc4f..215831b 100644 --- a/frontend/src/components/UI/ThemeSwitcher.tsx +++ b/frontend/src/components/UI/ThemeSwitcher.tsx @@ -1,7 +1,6 @@ 'use client'; -import React from 'react'; -import { useThemeStore, Theme } from '@/hooks/themeStore'; +import { useThemeStore, Theme } from '@/hooks/useTheme'; export default function ThemeSwitcher() { const { theme, setTheme } = useThemeStore(); diff --git a/frontend/src/components/games/Connect4Board.tsx b/frontend/src/components/games/Connect4Board.tsx index 7faa38c..9296a24 100644 --- a/frontend/src/components/games/Connect4Board.tsx +++ b/frontend/src/components/games/Connect4Board.tsx @@ -1,40 +1,38 @@ -import { useState } from 'react'; import { GameBoardProps } from '@/types/gameTypes'; -import { useUserStore } from '@/hooks/userStore'; +import { useUserStore } from '@/hooks/useUserStore'; import { cn } from '@/lib/utils'; +import { useGameBoard } from '@/hooks/useGameBoard'; export function Connect4Board({ gameState, makeMove }: GameBoardProps) { - const [hoveredCol, setHoveredCol] = useState(-1); const username = useUserStore(state => state.username); const yourIdx = gameState.players.indexOf(username); const isYourTurn = gameState.status == 'in_progress' && gameState.turn === yourIdx; - const handleClick = () => { - if (!isYourTurn || hoveredCol < 0) return; - makeMove({ to: { row: 0, col: hoveredCol } }); + const handleCellClick = (col: number) => { + console.log(col); + if (!isYourTurn) return; + makeMove({ to: { row: 0, col } }); }; - return
setHoveredCol(-1)} + const { getCellProps, hoveredCell } = useGameBoard({ + onCellClick: handleCellClick, + }); + + return
{[...Array(7)].map((_, col) => (
setHoveredCol(col)} - onPointerLeave={() => setHoveredCol(-1)} + {...getCellProps(col)} > - {hoveredCol === col && ( + {hoveredCell === col && (
)} diff --git a/frontend/src/components/games/CreateGame.tsx b/frontend/src/components/games/CreateGame.tsx index 90f3d73..a8cdce5 100644 --- a/frontend/src/components/games/CreateGame.tsx +++ b/frontend/src/components/games/CreateGame.tsx @@ -1,5 +1,3 @@ -'use client'; - import { useState } from 'react'; import { GameName, GAME_NAMES } from '@/types/wsTypes'; import { GAME_DISPLAY_NAMES } from '@/config/consts'; diff --git a/frontend/src/components/games/GameStatus.tsx b/frontend/src/components/games/GameStatus.tsx index 5ed0196..c2aea62 100644 --- a/frontend/src/components/games/GameStatus.tsx +++ b/frontend/src/components/games/GameStatus.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useUserStore } from '@/hooks/userStore'; +import { useUserStore } from '@/hooks/useUserStore'; import { BoardGameState } from '@/types/wsTypes'; import Button from '@/components/UI/Button'; diff --git a/frontend/src/components/games/TicTacToeBoard.tsx b/frontend/src/components/games/TicTacToeBoard.tsx index 14592be..48e53a0 100644 --- a/frontend/src/components/games/TicTacToeBoard.tsx +++ b/frontend/src/components/games/TicTacToeBoard.tsx @@ -1,26 +1,32 @@ 'use client'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { GameBoardProps } from '@/types/gameTypes'; -import { useUserStore } from '@/hooks/userStore'; +import { useUserStore } from '@/hooks/useUserStore'; import { cn } from '@/lib/utils'; import { X, Circle } from 'lucide-react'; +import { useGameBoard } from '@/hooks/useGameBoard'; export function TicTacToeBoard({ gameState, makeMove }: GameBoardProps) { - const [hoveredCell, setHoveredCell] = useState<{ row: number, col: number } | null>(null); const username = useUserStore(state => state.username); const yourIdx = gameState.players.indexOf(username); const isYourTurn = gameState.status === 'in_progress' && gameState.turn === yourIdx; - const handleCellClick = useCallback((row: number, col: number) => { + const handleCellClick = useCallback((cell: number) => { + const row = Math.floor(cell / 3); + const col = cell % 3; if (gameState.status === 'in_progress' && gameState.board[row][col] === 0 && isYourTurn) makeMove({ to: { row, col } }); }, [gameState.status, gameState.board, makeMove, isYourTurn]); + const { getCellProps, hoveredCell } = useGameBoard({ + onCellClick: handleCellClick, + }); + return
{gameState.board.map((cellRow, row) => cellRow.map((cell, col) => { - const isHovered = hoveredCell?.row === row && hoveredCell?.col === col; + const isHovered = row * 3 + col === hoveredCell; return
0 && yourIdx !== -1 && cell !== yourIdx + 1, } )} - onClick={() => handleCellClick(row, col)} - onMouseEnter={() => setHoveredCell({ row, col })} - onMouseLeave={() => setHoveredCell(null)} + {...getCellProps(row * 3 + col)} > {isHovered &&
} - {cell === 1 ? - : cell === 2 ? + {cell === 1 ? + : cell === 2 ? : null}
; }) diff --git a/frontend/src/components/games/chess/ChessBoard.tsx b/frontend/src/components/games/chess/ChessBoard.tsx index 27c48a1..a350c49 100644 --- a/frontend/src/components/games/chess/ChessBoard.tsx +++ b/frontend/src/components/games/chess/ChessBoard.tsx @@ -1,71 +1,40 @@ 'use client'; -import { useState, useRef, useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import { GameBoardProps } from '@/types/gameTypes'; -import { useUserStore } from '@/hooks/userStore'; +import { useUserStore } from '@/hooks/useUserStore'; import { cn } from '@/lib/utils'; import { numToPiece } from './pieceMapping'; - -type Position = { row: number; col: number }; - -interface DragState { - piece: number; - from: Position; - pos: { x: number; y: number }; -} +import { useGameBoard } from '@/hooks/useGameBoard'; export function ChessBoard({ gameState, makeMove }: GameBoardProps) { const username = useUserStore(state => state.username); - const hover = useRef(null); - const [drag, setDrag] = useState(null); - const idx = gameState.players.indexOf(username); const yourTurn = gameState.status === 'in_progress' && gameState.turn === idx; - const handlePointerDown = (e: React.PointerEvent, piece: number, row: number, col: number) => { - if (e.pointerType === 'mouse' && e.button !== 0) return; - e.preventDefault(); - setDrag({ - piece: piece, - from: { row, col }, - pos: { x: e.clientX, y: e.clientY }, - }); - }; - - const validSquares = useMemo(() => gameState.validMoves.filter(vMove => - vMove.from?.row === drag?.from.row && vMove.from?.col === drag?.from.col) - , [gameState.validMoves, drag?.from.row, drag?.from.col]); + const { getCellProps, hoveredCell, dragging } = useGameBoard({ + onCellDrop: (from, to) => { + if (!yourTurn) return; + const fromRow = Math.floor(from / 8); + const fromCol = from % 8; + const toRow = Math.floor(to / 8); + const toCol = to % 8; - useEffect(() => { - if (!drag) return; - const handlePointerMove = (e: PointerEvent) => { - setDrag(prev => ({ - ...prev!, - pos: { x: e.clientX, y: e.clientY }, - })); - }; - const handlePointerUp = () => { - const hoverSquare = hover.current; - if (hoverSquare && yourTurn && validSquares.some(vMove => - vMove.to.row === hoverSquare.row && vMove.to.col === hoverSquare.col)) {//todo: remove valid check, make sure backend enforces it - makeMove({ - from: drag.from, - to: hoverSquare, - }); - } - setDrag(null); - }; + if (gameState.validMoves.some(mv => + mv.from?.row === fromRow && mv.from?.col === fromCol && + mv.to.row === toRow && mv.to.col === toCol)) + makeMove({ from: { row: fromRow, col: fromCol }, to: { row: toRow, col: toCol } }); + } + }); - document.addEventListener('pointermove', handlePointerMove); - document.addEventListener('pointerup', handlePointerUp, { once: true }); - return () => { - document.removeEventListener('pointermove', handlePointerMove); - document.removeEventListener('pointerup', handlePointerUp); - }; - }, [drag, makeMove, validSquares, yourTurn]); + const validSquares = useMemo(() => gameState.validMoves.filter(vMove => { + if (dragging.from === null) return false; + const row = Math.floor(dragging.from / 8); + const col = dragging.from % 8; + return vMove.from?.row === row && vMove.from?.col === col; + }), [gameState.validMoves, dragging.from]); - return
-

attention: work in progress

+ return
{gameState.board.map((cellRow, row) => cellRow.map((cell, col) => { const isLight = (row + col) % 2 === 0; @@ -74,22 +43,33 @@ export function ChessBoard({ gameState, makeMove }: GameBoardProps) { return
move.to.row === row && move.to.col === col) ? isLight ? 'bg-primary/33' : 'bg-primary/66' : isLight ? 'bg-secondary/10' : 'bg-secondary/50', + hoveredCell === row * 8 + col && 'bg-accent/30', + dragging.from === row * 8 + col && 'bg-accent/60' )} - onPointerEnter={() => hover.current = { row, col }} - onPointerLeave={() => hover.current = null} - onPointerDown={(e) => handlePointerDown(e, cell, row, col)} + {...getCellProps(row * 8 + col)} > {Piece - && !(drag && drag.from.row === row && drag.from.col === col) - && } + && !(dragging.from === row * 8 + col) + && }
}) )} + {dragging.from !== null && (() => { + const row = Math.floor(dragging.from / 8); + const col = dragging.from % 8; + const pieceNum = gameState.board[row][col]; + const Piece = numToPiece[pieceNum]; + if (!Piece) return null; + return ; + })()}
; } diff --git a/frontend/src/config/consts.ts b/frontend/src/config/consts.ts index dbe0ca0..4b0013e 100644 --- a/frontend/src/config/consts.ts +++ b/frontend/src/config/consts.ts @@ -1,6 +1,6 @@ import { GameName } from '@/types/wsTypes'; -export const RECONNECT_INITIAL_DELAY = 5000; +export const RECONNECT_INITIAL_DELAY = 100; export const RECONNECT_MAX_DELAY = 10000; export const RECONNECT_MAX_ATTEMPTS = 10; diff --git a/frontend/src/hooks/useDraw.ts b/frontend/src/hooks/useDraw.ts index dd59b96..15443a2 100644 --- a/frontend/src/hooks/useDraw.ts +++ b/frontend/src/hooks/useDraw.ts @@ -1,7 +1,7 @@ 'use client'; import { useRef, useEffect, useState } from 'react'; -import { useWebSocket } from './webSocket'; +import { useWebSocket } from './useWebsocket'; import type { DrawPayload } from '@/types/wsTypes'; import { msgRawSignal } from '@/types/wsTypes'; import { DRAW_START_COLOR, DRAW_STROKE_INTERVAL, DRAW_START_WIDTH } from '@/config/consts'; diff --git a/frontend/src/hooks/useGameBoard.ts b/frontend/src/hooks/useGameBoard.ts new file mode 100644 index 0000000..e660254 --- /dev/null +++ b/frontend/src/hooks/useGameBoard.ts @@ -0,0 +1,113 @@ +import { useState, useRef, useEffect } from 'react'; + +type useBoardProps = { + onCellClick?: (cellIndex: number) => void; + onCellDrop?: (fromIndex: number, toIndex: number) => void; + touchOffset?: { x: number; y: number }; +}; + +const noDrag = { from: null, pos: { x: 0, y: 0 } }; + +export function useGameBoard({ onCellClick, onCellDrop, touchOffset = { x: 0, y: 10 } }: useBoardProps) { + const [hoveredCell, setHoveredCell] = useState(null); + const [dragging, setDrag] = useState<{ from: number | null; pos: { x: number; y: number } }>(noDrag); + const sameCell = useRef(true); // for cursor cell click + + const lastInputType = useRef(null); + const setInputType = (type: React.PointerEvent['pointerType']) => { + if (lastInputType.current && lastInputType.current !== type) setDrag({ ...noDrag }); + lastInputType.current = type; + }; + + /* causing problems with mouse drag. Prolly need to be used with manual hit test */ + // const captured = useRef<{ ele: Element | null; ptr: number | null }> + // ({ ele: null, ptr: null }); + // const capture = (e: React.PointerEvent) => { + // e.currentTarget.setPointerCapture(e.pointerId); + // captured.current = { ele: e.currentTarget, ptr: e.pointerId }; + // } + // const release = () => { + // if (captured.current.ele && captured.current.ptr !== null) { + // try { + // captured.current.ele.releasePointerCapture(captured.current.ptr); + // } finally { + // captured.current = { ele: null, ptr: null }; + // } + // } + // } + + + const getCellProps = (cellIndex: number) => ({ + onPointerDown: (e: React.PointerEvent) => { + e.stopPropagation() + setInputType(e.pointerType); + // capture(e); + if (e.pointerType === 'touch') { + if (onCellClick) onCellClick(cellIndex); + + if (dragging.from === null) { + const rect = e.currentTarget.getBoundingClientRect(); + const x = rect.left + rect.width / 2 + touchOffset.x; + const y = rect.top + rect.height / 2 - touchOffset.y; + setDrag({ from: cellIndex, pos: { x, y } }) + } else { + setDrag({ ...noDrag }) + if (onCellDrop) onCellDrop(dragging.from, cellIndex); + } + } else { + setDrag({ from: cellIndex, pos: { x: e.clientX, y: e.clientY } }); + sameCell.current = true; + } + }, + onPointerUp: (e: React.PointerEvent) => { + e.stopPropagation() + // release(); + setInputType(e.pointerType); + if (e.pointerType === 'touch') return; + + if (onCellClick && sameCell.current) + onCellClick(cellIndex); + if (onCellDrop && dragging.from !== null && hoveredCell !== null) + onCellDrop(dragging.from, hoveredCell); + setDrag({ ...noDrag }); + }, + + onPointerEnter: () => { setHoveredCell(cellIndex) }, + onPointerLeave: () => { + setHoveredCell(null); + sameCell.current = false; + }, + }); + + // + useEffect(() => { + if (dragging.from === null) return + const onPointerMove = (e: PointerEvent) => { + if (e.pointerType === 'touch') return; + setDrag(d => ({ ...d, pos: { x: e.clientX, y: e.clientY } })) + } + + const reset = () => { + // release(); + setDrag({ ...noDrag }) + } + + //cancel drag if outside. Up needed for mouse and down for touch + window.addEventListener('pointermove', onPointerMove); + window.addEventListener('pointerup', reset); + window.addEventListener('pointerdown', reset); + window.addEventListener('pointercancel', reset); + return () => { + window.removeEventListener('pointermove', onPointerMove); + window.removeEventListener('pointerup', reset); + window.removeEventListener('pointerdown', reset); + window.removeEventListener('pointercancel', reset); + }; + }, [dragging.from]); + + return { + hoveredCell, + dragging, + getCellProps, + }; +} \ No newline at end of file diff --git a/frontend/src/hooks/useGroupCall.ts b/frontend/src/hooks/useGroupCall.ts index c1e4202..404ac77 100644 --- a/frontend/src/hooks/useGroupCall.ts +++ b/frontend/src/hooks/useGroupCall.ts @@ -1,8 +1,8 @@ 'use client'; import { useState, useReducer, useCallback, useEffect } from 'react'; -import { useWebSocket } from '@/hooks/webSocket'; -import { useUserStore } from '@/hooks/userStore'; +import { useWebSocket } from '@/hooks/useWebsocket'; +import { useUserStore } from '@/hooks/useUserStore'; import { VidSignalMsg } from '@/types/wsTypes'; interface Peer { diff --git a/frontend/src/hooks/themeStore.ts b/frontend/src/hooks/useTheme.ts similarity index 96% rename from frontend/src/hooks/themeStore.ts rename to frontend/src/hooks/useTheme.ts index b8d80ec..e811b93 100644 --- a/frontend/src/hooks/themeStore.ts +++ b/frontend/src/hooks/useTheme.ts @@ -72,7 +72,7 @@ export const useThemeStore = create()( ) ); -// Optional: Listen for system preference changes and update if theme is 'system' +//for handling system pref change if (typeof window !== 'undefined') { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { const currentTheme = useThemeStore.getState().theme; diff --git a/frontend/src/hooks/userStore.ts b/frontend/src/hooks/useUserStore.ts similarity index 100% rename from frontend/src/hooks/userStore.ts rename to frontend/src/hooks/useUserStore.ts diff --git a/frontend/src/hooks/useBoardGame.ts b/frontend/src/hooks/useWSGame.ts similarity index 95% rename from frontend/src/hooks/useBoardGame.ts rename to frontend/src/hooks/useWSGame.ts index 335e334..285a12f 100644 --- a/frontend/src/hooks/useBoardGame.ts +++ b/frontend/src/hooks/useWSGame.ts @@ -1,8 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; -import { useWebSocket } from './webSocket'; +import { useWebSocket } from './useWebsocket'; import { BoardGameState, GameMove, GameName } from '@/types/wsTypes'; -export default function useBoardGame() { +export default function useWebSocketGame() { const { sendGameMsg, setGameHandler } = useWebSocket(); const [isLoading, setIsLoading] = useState(false); diff --git a/frontend/src/hooks/webSocket.ts b/frontend/src/hooks/useWebsocket.ts similarity index 97% rename from frontend/src/hooks/webSocket.ts rename to frontend/src/hooks/useWebsocket.ts index 8d2c2a7..db0bc6a 100644 --- a/frontend/src/hooks/webSocket.ts +++ b/frontend/src/hooks/useWebsocket.ts @@ -1,7 +1,7 @@ 'use client'; import { useEffect } from 'react'; -import { useUserStore } from './userStore'; +import { useUserStore } from './useUserStore'; import * as t from '@/types/wsTypes'; import { create } from 'zustand'; import { RECONNECT_INITIAL_DELAY, WS_URL } from '@/config/consts'; @@ -117,11 +117,6 @@ export const useWebSocket = create()((set, get) => ({ } } - ws.onerror = (event) => { - console.error('WS error:', event); - set({ error: 'WS error' }); - }; - ws.onclose = (event) => { console.debug('WS closed:', event.code, event.reason); set({ error: `WS disconnected: code ${event.code}, reason: ${event.reason || 'Unknown'}.` });