Skip to content

Commit f2aa4c5

Browse files
Merge pull request #129 from CSSLab/dev
Add user settings page + ux changes
2 parents 4d5b7f2 + a9a0a00 commit f2aa4c5

File tree

20 files changed

+1308
-5
lines changed

20 files changed

+1308
-5
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import '@testing-library/jest-dom'
2+
import { render, screen, fireEvent } from '@testing-library/react'
3+
import { SoundSettings } from 'src/components/Settings/SoundSettings'
4+
import { SettingsProvider } from 'src/contexts/SettingsContext'
5+
import { chessSoundManager } from 'src/lib/chessSoundManager'
6+
7+
// Mock the chess sound manager
8+
jest.mock('src/lib/chessSoundManager', () => ({
9+
chessSoundManager: {
10+
playMoveSound: jest.fn(),
11+
},
12+
useChessSoundManager: () => ({
13+
playMoveSound: jest.fn(),
14+
ready: true,
15+
}),
16+
}))
17+
18+
// Mock localStorage
19+
const localStorageMock = {
20+
getItem: jest.fn(),
21+
setItem: jest.fn(),
22+
removeItem: jest.fn(),
23+
clear: jest.fn(),
24+
}
25+
Object.defineProperty(window, 'localStorage', {
26+
value: localStorageMock,
27+
})
28+
29+
describe('SoundSettings Component', () => {
30+
beforeEach(() => {
31+
localStorageMock.getItem.mockReturnValue(
32+
JSON.stringify({ soundEnabled: true, chessboardTheme: 'brown' }),
33+
)
34+
})
35+
36+
afterEach(() => {
37+
jest.clearAllMocks()
38+
})
39+
40+
it('renders sound settings with toggle enabled by default', () => {
41+
render(
42+
<SettingsProvider>
43+
<SoundSettings />
44+
</SettingsProvider>,
45+
)
46+
47+
expect(screen.getByText('Sound Settings')).toBeInTheDocument()
48+
expect(screen.getByText('Enable Move Sounds')).toBeInTheDocument()
49+
expect(screen.getByRole('checkbox')).toBeChecked()
50+
})
51+
52+
it('shows test buttons when sound is enabled', () => {
53+
render(
54+
<SettingsProvider>
55+
<SoundSettings />
56+
</SettingsProvider>,
57+
)
58+
59+
expect(screen.getByText('Move Sound')).toBeInTheDocument()
60+
expect(screen.getByText('Capture Sound')).toBeInTheDocument()
61+
})
62+
63+
it('saves settings to localStorage when toggle is changed', () => {
64+
render(
65+
<SettingsProvider>
66+
<SoundSettings />
67+
</SettingsProvider>,
68+
)
69+
70+
const checkbox = screen.getByRole('checkbox')
71+
fireEvent.click(checkbox)
72+
73+
expect(localStorageMock.setItem).toHaveBeenCalledWith(
74+
'maia-user-settings',
75+
JSON.stringify({ soundEnabled: false, chessboardTheme: 'brown' }),
76+
)
77+
})
78+
})

src/components/Analysis/AnalysisConfigModal.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react'
1+
import React, { useState, useEffect } from 'react'
22
import { motion } from 'framer-motion'
33

44
interface Props {
@@ -16,6 +16,18 @@ export const AnalysisConfigModal: React.FC<Props> = ({
1616
}) => {
1717
const [selectedDepth, setSelectedDepth] = useState(initialDepth)
1818

19+
useEffect(() => {
20+
if (isOpen) {
21+
document.body.style.overflow = 'hidden'
22+
} else {
23+
document.body.style.overflow = 'unset'
24+
}
25+
26+
return () => {
27+
document.body.style.overflow = 'unset'
28+
}
29+
}, [isOpen])
30+
1931
const depthOptions = [
2032
{
2133
value: 12,

src/components/Analysis/AnalysisProgressOverlay.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useEffect } from 'react'
22
import { motion } from 'framer-motion'
33
import { GameAnalysisProgress } from 'src/hooks/useAnalysisController/useAnalysisController'
44

@@ -16,6 +16,18 @@ export const AnalysisProgressOverlay: React.FC<Props> = ({
1616
? Math.round((progress.currentMoveIndex / progress.totalMoves) * 100)
1717
: 0
1818

19+
useEffect(() => {
20+
if (progress.isAnalyzing) {
21+
document.body.style.overflow = 'hidden'
22+
} else {
23+
document.body.style.overflow = 'unset'
24+
}
25+
26+
return () => {
27+
document.body.style.overflow = 'unset'
28+
}
29+
}, [progress.isAnalyzing])
30+
1931
if (!progress.isAnalyzing) return null
2032

2133
return (

src/components/Common/Header.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ export const Header: React.FC = () => {
7777
>
7878
Profile
7979
</Link>
80+
<Link
81+
href="/settings"
82+
className="flex w-full items-center justify-start px-3 py-2 hover:bg-background-3"
83+
>
84+
Settings
85+
</Link>
8086
<button
8187
onClick={logout}
8288
className="flex w-full items-center justify-start px-3 py-2 hover:bg-background-3"
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React from 'react'
2+
import { useSettings } from 'src/contexts/SettingsContext'
3+
import Chessground from '@react-chess/chessground'
4+
5+
type ChessboardTheme =
6+
| 'brown'
7+
| 'blue'
8+
| 'blue2'
9+
| 'blue3'
10+
| 'blue-marble'
11+
| 'canvas2'
12+
| 'wood'
13+
| 'wood2'
14+
| 'wood3'
15+
| 'wood4'
16+
| 'maple'
17+
| 'maple2'
18+
| 'leather'
19+
| 'green'
20+
| 'pink-pyramid'
21+
| 'marble'
22+
| 'green-plastic'
23+
| 'grey'
24+
| 'metal'
25+
| 'olive'
26+
| 'newspaper'
27+
| 'purple'
28+
| 'purple-diag'
29+
| 'ic'
30+
| 'horsey'
31+
| 'wood-worn'
32+
| 'putt-putt'
33+
| 'cocoa'
34+
| 'parchment'
35+
36+
// Flattened list of all themes
37+
const ALL_THEMES: ChessboardTheme[] = [
38+
'brown',
39+
'blue',
40+
'blue2',
41+
'blue3',
42+
'blue-marble',
43+
'canvas2',
44+
'wood',
45+
'wood2',
46+
'wood3',
47+
'wood4',
48+
'maple',
49+
'maple2',
50+
'green',
51+
'marble',
52+
'green-plastic',
53+
'grey',
54+
'metal',
55+
'newspaper',
56+
'ic',
57+
'purple',
58+
'purple-diag',
59+
'pink-pyramid',
60+
'leather',
61+
'olive',
62+
'horsey',
63+
'wood-worn',
64+
'putt-putt',
65+
'cocoa',
66+
'parchment',
67+
]
68+
69+
export const ChessboardSettings: React.FC = () => {
70+
const { settings, updateSetting } = useSettings()
71+
72+
const handleThemeChange = (theme: ChessboardTheme) => {
73+
updateSetting('chessboardTheme', theme)
74+
}
75+
76+
return (
77+
<div className="flex flex-col gap-4 rounded bg-background-1 p-6">
78+
<div className="flex flex-col items-start justify-between">
79+
<h3 className="text-lg font-semibold">Chessboard Theme</h3>
80+
<p className="text-sm text-secondary">
81+
Choose your preferred chessboard style. Changes will apply to all
82+
chess boards across the platform.
83+
</p>
84+
</div>
85+
86+
<div className="flex flex-col gap-4">
87+
{/* Theme Grid */}
88+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
89+
{ALL_THEMES.map((theme) => (
90+
<label
91+
key={theme}
92+
className={`flex cursor-pointer items-center justify-center rounded border p-2 transition-colors ${
93+
settings.chessboardTheme === theme
94+
? 'border-human-4 bg-human-3/20'
95+
: 'border-white/10 bg-background-2/60 hover:bg-background-2'
96+
}`}
97+
>
98+
<input
99+
type="radio"
100+
name="chessboard-theme"
101+
value={theme}
102+
checked={settings.chessboardTheme === theme}
103+
onChange={() => handleThemeChange(theme)}
104+
className="sr-only"
105+
/>
106+
<div
107+
className={`theme-preview-${theme} aspect-square h-16 w-16 overflow-hidden rounded`}
108+
>
109+
<Chessground
110+
contained
111+
config={{
112+
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
113+
viewOnly: true,
114+
coordinates: false,
115+
}}
116+
/>
117+
</div>
118+
</label>
119+
))}
120+
</div>
121+
122+
<div className="flex items-start gap-2 rounded bg-background-2/60 px-3 py-2">
123+
<span className="material-symbols-outlined inline !text-base text-secondary">
124+
info
125+
</span>
126+
<p className="text-sm text-secondary">
127+
Theme changes take effect immediately and will be remembered across
128+
browser sessions. Preview shows how the board will appear in games.
129+
</p>
130+
</div>
131+
</div>
132+
</div>
133+
)
134+
}

0 commit comments

Comments
 (0)