@@ -16,10 +16,14 @@ export const GameSettingsPage: React.FC = () => {
1616 const [ roles , setRoles ] = useState < string [ ] > ( [ ] ) ;
1717 const [ roleCounts , setRoleCounts ] = useState < Record < string , number > > ( { } ) ;
1818
19- // New State Variables
19+ // State for initial values (to track changes)
20+ const [ initialPlayerCount , setInitialPlayerCount ] = useState < number > ( 12 ) ;
21+
2022 const [ playerCount , setPlayerCount ] = useState < number > ( 12 ) ;
2123 const [ selectedRole , setSelectedRole ] = useState < string > ( '' ) ;
2224 const [ updatingRoles , setUpdatingRoles ] = useState ( false ) ;
25+ const [ updatingPlayerCount , setUpdatingPlayerCount ] = useState ( false ) ;
26+ const [ pendingFields , setPendingFields ] = useState < Record < string , boolean > > ( { } ) ;
2327
2428 const AVAILABLE_ROLES = [
2529 "平民" , "狼人" , "女巫" , "預言家" , "獵人" ,
@@ -30,14 +34,11 @@ export const GameSettingsPage: React.FC = () => {
3034
3135 const isFirstLoad = useRef ( true ) ;
3236
33- // Auto-save effect
34- useEffect ( ( ) => {
35- if ( isFirstLoad . current ) {
36- isFirstLoad . current = false ;
37- return ;
38- }
37+ const playerCountChanged = playerCount !== initialPlayerCount ;
3938
40- if ( loading ) return ;
39+ // Auto-save effect for toggles
40+ useEffect ( ( ) => {
41+ if ( isFirstLoad . current || loading ) return ;
4142
4243 const saveSettings = async ( ) => {
4344 if ( ! guildId ) return ;
@@ -47,12 +48,13 @@ export const GameSettingsPage: React.FC = () => {
4748 muteAfterSpeech,
4849 doubleIdentities
4950 } ) ;
50- } catch ( e ) {
51- console . error ( "Failed to update settings" , e ) ;
52- } finally {
5351 setJustSaved ( true ) ;
54- setTimeout ( ( ) => setSaving ( false ) , 500 ) ;
5552 setTimeout ( ( ) => setJustSaved ( false ) , 2000 ) ;
53+ setPendingFields ( { } ) ;
54+ } catch ( e ) {
55+ console . error ( "Failed to auto-save settings" , e ) ;
56+ } finally {
57+ setSaving ( false ) ;
5658 }
5759 } ;
5860
@@ -76,6 +78,7 @@ export const GameSettingsPage: React.FC = () => {
7678 // Set player count from current players length
7779 if ( Array . isArray ( sessionData . players ) ) {
7880 setPlayerCount ( sessionData . players . length ) ;
81+ setInitialPlayerCount ( sessionData . players . length ) ;
7982 }
8083
8184 setRoles ( rolesData . filter ( ( r : unknown ) : r is string => typeof r === 'string' ) || [ ] ) ;
@@ -104,12 +107,13 @@ export const GameSettingsPage: React.FC = () => {
104107 } , [ roles ] ) ;
105108
106109 const handleAddRole = async ( role : string ) => {
107- if ( ! guildId || updatingRoles ) return ;
110+ if ( ! guildId || updatingRoles || ! role . trim ( ) ) return ;
108111 setUpdatingRoles ( true ) ;
109112 try {
110- await api . addRole ( guildId , role , 1 ) ;
113+ await api . addRole ( guildId , role . trim ( ) , 1 ) ;
111114 const newRoles = await api . getRoles ( guildId ) as string [ ] ;
112115 setRoles ( newRoles . filter ( ( r : unknown ) : r is string => typeof r === 'string' ) || [ ] ) ;
116+ setSelectedRole ( '' ) ;
113117 } catch ( e ) {
114118 console . error ( "Failed to add role" , e ) ;
115119 } finally {
@@ -141,15 +145,31 @@ export const GameSettingsPage: React.FC = () => {
141145 } ;
142146
143147 const handlePlayerCountUpdate = async ( ) => {
144- if ( ! guildId ) return ;
148+ if ( ! guildId || updatingPlayerCount ) return ;
149+ setUpdatingPlayerCount ( true ) ;
145150 try {
146151 await api . setPlayerCount ( guildId , playerCount ) ;
147- loadSettings ( ) ;
152+ setInitialPlayerCount ( playerCount ) ;
153+ // No need to loadSettings() here, as only initialPlayerCount needs to be updated
148154 } catch ( error : any ) {
149155 console . error ( "Update failed" , error ) ;
156+ } finally {
157+ setUpdatingPlayerCount ( false ) ;
150158 }
151159 } ;
152160
161+ const toggleMute = ( checked : boolean ) => {
162+ if ( saving || pendingFields [ 'mute' ] ) return ;
163+ setMuteAfterSpeech ( checked ) ;
164+ setPendingFields ( prev => ( { ...prev , mute : true } ) ) ;
165+ } ;
166+
167+ const toggleDoubleIdentities = ( checked : boolean ) => {
168+ if ( saving || pendingFields [ 'double' ] ) return ;
169+ setDoubleIdentities ( checked ) ;
170+ setPendingFields ( prev => ( { ...prev , double : true } ) ) ;
171+ } ;
172+
153173 if ( loading ) {
154174 return (
155175 < div className = "flex justify-center items-center h-64" >
@@ -163,9 +183,26 @@ export const GameSettingsPage: React.FC = () => {
163183 < div className = "space-y-8" >
164184 { /* General Settings */ }
165185 < div className = "space-y-4" >
166- < h3 className = "text-sm font-bold text-slate-500 uppercase tracking-wider border-b border-slate-200 dark:border-slate-800 pb-2" >
167- { t ( 'settings.general' ) }
168- </ h3 >
186+ < div
187+ className = "flex items-center justify-between border-b border-slate-200 dark:border-slate-800 pb-2" >
188+ < div className = "flex items-center gap-3" >
189+ < h3 className = "text-sm font-bold text-slate-500 uppercase tracking-wider" >
190+ { t ( 'settings.general' ) }
191+ </ h3 >
192+ { ( saving || justSaved || Object . keys ( pendingFields ) . length > 0 ) && (
193+ < span
194+ className = { `text-xs flex items-center gap-1.5 font-medium px-2 py-0.5 rounded-full transition-all ${ ( saving || Object . keys ( pendingFields ) . length > 0 )
195+ ? 'text-slate-500 bg-slate-100 dark:bg-slate-800 animate-pulse'
196+ : 'text-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
197+ } `} >
198+ { ( saving || Object . keys ( pendingFields ) . length > 0 ) ?
199+ < Loader2 className = "w-3.5 h-3.5 animate-spin" /> :
200+ < Check className = "w-3.5 h-3.5" /> }
201+ { ( saving || Object . keys ( pendingFields ) . length > 0 ) ? t ( 'messages.saving' ) : t ( 'messages.saved' ) }
202+ </ span >
203+ ) }
204+ </ div >
205+ </ div >
169206
170207 < div className = "flex items-center justify-between" >
171208 < div >
@@ -176,22 +213,14 @@ export const GameSettingsPage: React.FC = () => {
176213 </ span >
177214 </ div >
178215 < div className = "flex items-center gap-3" >
179- { ( saving || justSaved ) && (
180- < div className = "flex items-center justify-center w-5 h-5" >
181- { saving ? (
182- < Loader2 className = "w-4 h-4 animate-spin text-slate-400" />
183- ) : (
184- < Check className = "w-4 h-4 text-emerald-500" />
185- ) }
186- </ div >
187- ) }
216+ { pendingFields [ 'mute' ] && < Loader2 className = "w-4 h-4 animate-spin text-indigo-500" /> }
188217 < label
189- className = { `relative inline-flex items-center ${ saving ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' } ` } >
218+ className = { `relative inline-flex items-center ${ pendingFields [ 'mute' ] ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' } ` } >
190219 < input
191220 type = "checkbox"
192221 checked = { muteAfterSpeech }
193- onChange = { ( e ) => ! saving && setMuteAfterSpeech ( e . target . checked ) }
194- disabled = { saving }
222+ onChange = { ( e ) => toggleMute ( e . target . checked ) }
223+ disabled = { saving || pendingFields [ 'mute' ] }
195224 className = "sr-only peer"
196225 />
197226 < div
@@ -209,22 +238,14 @@ export const GameSettingsPage: React.FC = () => {
209238 </ span >
210239 </ div >
211240 < div className = "flex items-center gap-3" >
212- { ( saving || justSaved ) && (
213- < div className = "flex items-center justify-center w-5 h-5" >
214- { saving ? (
215- < Loader2 className = "w-4 h-4 animate-spin text-slate-400" />
216- ) : (
217- < Check className = "w-4 h-4 text-emerald-500" />
218- ) }
219- </ div >
220- ) }
241+ { pendingFields [ 'double' ] && < Loader2 className = "w-4 h-4 animate-spin text-indigo-500" /> }
221242 < label
222- className = { `relative inline-flex items-center ${ saving ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' } ` } >
243+ className = { `relative inline-flex items-center ${ pendingFields [ 'double' ] ? 'cursor-not-allowed opacity-50' : 'cursor-pointer' } ` } >
223244 < input
224245 type = "checkbox"
225246 checked = { doubleIdentities }
226- onChange = { ( e ) => ! saving && setDoubleIdentities ( e . target . checked ) }
227- disabled = { saving }
247+ onChange = { ( e ) => toggleDoubleIdentities ( e . target . checked ) }
248+ disabled = { saving || pendingFields [ 'double' ] }
228249 className = "sr-only peer"
229250 />
230251 < div
@@ -239,29 +260,60 @@ export const GameSettingsPage: React.FC = () => {
239260 < h3 className = "text-sm font-bold text-slate-500 uppercase tracking-wider border-b border-slate-200 dark:border-slate-800 pb-2" >
240261 { t ( 'settings.playerCount' ) }
241262 </ h3 >
242- < div className = "flex items-end gap-4" >
263+ < div
264+ className = "flex items-center gap-6 bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700" >
243265 < div className = "flex-1" >
244266 < label className = "block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1" >
245267 { t ( 'settings.totalPlayers' ) }
246268 </ label >
247- < input
248- type = "number"
249- min = "1"
250- max = "50"
251- value = { playerCount }
252- onChange = { ( e ) => setPlayerCount ( parseInt ( e . target . value ) || 0 ) }
253- className = "w-full bg-slate-100 dark:bg-slate-800 border-none rounded-lg px-4 py-2 text-slate-900 dark:text-slate-200 focus:ring-2 focus:ring-indigo-500"
254- />
255- < p className = "mt-1 text-xs text-slate-500 dark:text-slate-400" >
269+ < p className = "text-xs text-slate-500 dark:text-slate-400" >
256270 { t ( 'settings.playerCountDesc' ) }
257271 </ p >
258272 </ div >
259- < button
260- onClick = { handlePlayerCountUpdate }
261- className = "bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg transition-colors h-10 mb-0.5"
262- >
263- { t ( 'buttons.update' ) }
264- </ button >
273+
274+ { ( playerCountChanged || updatingPlayerCount ) && (
275+ < button
276+ onClick = { handlePlayerCountUpdate }
277+ disabled = { updatingPlayerCount || ! playerCountChanged }
278+ className = { `px-6 py-2 rounded-xl transition-all shadow-lg font-bold text-sm flex items-center gap-2 ${ playerCountChanged
279+ ? 'bg-indigo-600 hover:bg-indigo-700 text-white shadow-indigo-200 dark:shadow-none'
280+ : 'bg-slate-100 text-slate-400 cursor-not-allowed dark:bg-slate-800'
281+ } `}
282+ >
283+ { updatingPlayerCount ? < Loader2 className = "w-4 h-4 animate-spin" /> :
284+ < Check className = "w-4 h-4" /> }
285+ { t ( 'buttons.update' ) }
286+ </ button >
287+ ) }
288+
289+ < div
290+ className = "flex items-center gap-4 bg-white dark:bg-slate-900 p-2 rounded-lg border border-slate-200 dark:border-slate-700 shadow-sm" >
291+ < button
292+ onClick = { ( ) => playerCount > 1 && setPlayerCount ( prev => prev - 1 ) }
293+ disabled = { updatingPlayerCount || playerCount <= 1 }
294+ className = "w-10 h-10 flex items-center justify-center text-slate-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
295+ >
296+ < Minus className = "w-6 h-6" />
297+ </ button >
298+
299+ < div className = "flex flex-col items-center min-w-[3rem]" >
300+ { updatingPlayerCount ? (
301+ < Loader2 className = "w-6 h-6 animate-spin text-indigo-500" />
302+ ) : (
303+ < span className = "text-2xl font-black text-slate-900 dark:text-white tabular-nums" >
304+ { playerCount }
305+ </ span >
306+ ) }
307+ </ div >
308+
309+ < button
310+ onClick = { ( ) => playerCount < 50 && setPlayerCount ( prev => prev + 1 ) }
311+ disabled = { updatingPlayerCount || playerCount >= 50 }
312+ className = "w-10 h-10 flex items-center justify-center text-slate-500 hover:text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded-lg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
313+ >
314+ < Plus className = "w-6 h-6" />
315+ </ button >
316+ </ div >
265317 </ div >
266318 </ div >
267319
@@ -302,7 +354,7 @@ export const GameSettingsPage: React.FC = () => {
302354 </ div >
303355 < button
304356 onClick = { ( ) => handleAddRole ( selectedRole ) }
305- disabled = { updatingRoles }
357+ disabled = { updatingRoles || ! selectedRole . trim ( ) }
306358 className = "flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg transition-colors"
307359 >
308360 { updatingRoles ? < Loader2 className = "w-5 h-5 animate-spin" /> : < Plus className = "w-5 h-5" /> }
0 commit comments