From ba74941d6bfe83bb7b59626e7ee93851d1375bfc Mon Sep 17 00:00:00 2001 From: issue-solver-bot Date: Wed, 26 Aug 2026 00:38:51 +0000 Subject: [PATCH] fix: resolve issue #1055 --- frontend/app/layout.tsx | 1 + frontend/app/settings/page.tsx | 5 + .../components/GamePreferencesSettings.tsx | 208 ++++++++++++++++++ frontend/context/GamePreferencesContext.tsx | 111 ++++++++++ 4 files changed, 325 insertions(+) create mode 100644 frontend/components/GamePreferencesSettings.tsx create mode 100644 frontend/context/GamePreferencesContext.tsx diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 667ddd6b..9b6a5a92 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -8,6 +8,7 @@ import { TransactionProvider } from "@/context/transactionContext"; import { ThemeProvider } from "next-themes"; import { BoardThemeProvider } from "@/context/ThemeContext"; import { SoundProvider } from "@/context/SoundContext"; +import { GamePreferencesProvider } from "@/context/GamePreferencesContext"; import { AuthProvider } from "@/context/authContext"; export const metadata: Metadata = { diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx index eec25898..4cef5ead 100644 --- a/frontend/app/settings/page.tsx +++ b/frontend/app/settings/page.tsx @@ -3,6 +3,7 @@ import React from 'react'; import AppearanceSettings from '@/components/AppearanceSettings'; import SoundSettings from '@/components/SoundSettings'; +import GamePreferencesSettings from '@/components/GamePreferencesSettings'; import { GameSidebar } from '@/components/GameSidebar'; import { Header } from '@/components/Header'; @@ -31,6 +32,10 @@ export default function SettingsPage() { + +
+ +
diff --git a/frontend/components/GamePreferencesSettings.tsx b/frontend/components/GamePreferencesSettings.tsx new file mode 100644 index 00000000..9c9927de --- /dev/null +++ b/frontend/components/GamePreferencesSettings.tsx @@ -0,0 +1,208 @@ +"use client"; + +import React from "react"; +import { + useGamePreferences, + PieceInputMethod, + AutoQueenMode, + LegalMoveDots, + BoardCoordinates, +} from "@/context/GamePreferencesContext"; + +interface Option { + id: T; + label: string; + description?: string; +} + +function RadioGroup({ + label, + value, + options, + onChange, + tooltip, +}: { + label: string; + value: T; + options: Option[]; + onChange: (value: T) => void; + tooltip?: string; +}) { + return ( +
+
+

{label}

+ {tooltip && ( + + ? + + {tooltip} + + + )} +
+
+ {options.map((option) => ( + + ))} +
+
+ ); +} + +function Toggle({ + label, + checked, + onChange, + tooltip, +}: { + label: string; + checked: boolean; + onChange: (checked: boolean) => void; + tooltip?: string; +}) { + return ( +
+
+ {label} + {tooltip && ( + + ? + + {tooltip} + + + )} +
+ +
+ ); +} + +export default function GamePreferencesSettings() { + const { preferences, setPreference } = useGamePreferences(); + + return ( +
+

+ Game Preferences +

+ + + label="Piece Input Method" + value={preferences.pieceInputMethod} + onChange={(v) => setPreference("pieceInputMethod", v)} + tooltip="Choose how you move pieces on the board." + options={[ + { id: "drag", label: "Drag & Drop", description: "Drag pieces to move" }, + { id: "click", label: "Click-Click", description: "Click source then target" }, + { id: "both", label: "Both", description: "Use either method" }, + ]} + /> + + + label="Auto-Queen on Promotion" + value={preferences.autoQueen} + onChange={(v) => setPreference("autoQueen", v)} + tooltip="Controls how pawn promotion is handled." + options={[ + { id: "always", label: "Always Queen", description: "Auto-promote to Queen" }, + { id: "prompt", label: "Always Prompt Choice", description: "Ask which piece to promote to" }, + { id: "premoves", label: "Auto-Queen in Pre-moves only", description: "Auto-queen only for pre-moves" }, + ]} + /> + + + label="Show Legal Move Dots" + value={preferences.showLegalMoveDots} + onChange={(v) => setPreference("showLegalMoveDots", v)} + tooltip="Display dots on squares where a selected piece can move." + options={[ + { id: "enabled", label: "Enabled", description: "Show legal move indicators" }, + { id: "disabled", label: "Disabled", description: "Hide legal move indicators" }, + ]} + /> + +
+ setPreference("confirmMoveCorrespondence", v)} + tooltip="Require explicit confirmation before sending a move in correspondence games." + /> +
+ + + label="Board Coordinates" + value={preferences.boardCoordinates} + onChange={(v) => setPreference("boardCoordinates", v)} + tooltip="Choose where to display board coordinate labels." + options={[ + { id: "inside", label: "Inside Board", description: "Show coordinates on the squares" }, + { id: "outside", label: "Outside Board", description: "Show coordinates around the board" }, + { id: "hidden", label: "Hidden", description: "Do not show coordinates" }, + ]} + /> + +

+ Changes are saved automatically and apply to all your games. +

+
+ ); +} diff --git a/frontend/context/GamePreferencesContext.tsx b/frontend/context/GamePreferencesContext.tsx new file mode 100644 index 00000000..7b3f1c0f --- /dev/null +++ b/frontend/context/GamePreferencesContext.tsx @@ -0,0 +1,111 @@ +"use client"; + +import React, { createContext, useContext, useState, useEffect } from "react"; + +export type PieceInputMethod = "drag" | "click" | "both"; +export type AutoQueenMode = "always" | "prompt" | "premoves"; +export type LegalMoveDots = "enabled" | "disabled"; +export type BoardCoordinates = "inside" | "outside" | "hidden"; + +export interface GamePreferences { + pieceInputMethod: PieceInputMethod; + autoQueen: AutoQueenMode; + showLegalMoveDots: LegalMoveDots; + confirmMoveCorrespondence: boolean; + boardCoordinates: BoardCoordinates; +} + +export const DEFAULT_PREFERENCES: GamePreferences = { + pieceInputMethod: "both", + autoQueen: "always", + showLegalMoveDots: "enabled", + confirmMoveCorrespondence: false, + boardCoordinates: "inside", +}; + +const STORAGE_KEY = "knightverse_game_preferences"; + +interface GamePreferencesContextProps { + preferences: GamePreferences; + setPreference: ( + key: K, + value: GamePreferences[K], + ) => void; + resetPreferences: () => void; +} + +const GamePreferencesContext = createContext< + GamePreferencesContextProps | undefined +>(undefined); + +export const GamePreferencesProvider: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => { + const [preferences, setPreferences] = useState( + DEFAULT_PREFERENCES, + ); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const parsed = JSON.parse(saved); + setPreferences({ ...DEFAULT_PREFERENCES, ...parsed }); + } + } catch { + // ignore malformed storage + } + setMounted(true); + }, []); + + const setPreference = ( + key: K, + value: GamePreferences[K], + ) => { + setPreferences((prev) => { + const next = { ...prev, [key]: value }; + if (mounted) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + } catch { + // storage unavailable + } + } + return next; + }); + }; + + const resetPreferences = () => { + setPreferences(DEFAULT_PREFERENCES); + if (mounted) { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + // storage unavailable + } + } + }; + + const value = { + preferences, + setPreference, + resetPreferences, + }; + + return ( + + {children} + + ); +}; + +export const useGamePreferences = () => { + const context = useContext(GamePreferencesContext); + if (!context) { + throw new Error( + "useGamePreferences must be used within a GamePreferencesProvider", + ); + } + return context; +};