Skip to content

Commit 8b1a2c8

Browse files
committed
better settings page
1 parent 78d31ce commit 8b1a2c8

3 files changed

Lines changed: 116 additions & 62 deletions

File tree

src/dashboard/src/features/game/components/GameSettingsPage.tsx

Lines changed: 112 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -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"/>}

src/dashboard/src/locales/zh-TW.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@
292292
"unassigned": "未指派",
293293
"player": "玩家",
294294
"speaking": "發言中",
295+
"saving": "儲存中...",
296+
"saved": "已儲存",
295297
"progress": "進度",
296298
"totalCount": "總數",
297299
"noRolesConfigured": "尚無角色配置",

src/main/kotlin/dev/robothanzo/werewolf/utils/DiscordActionRunner.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import java.util.concurrent.atomic.AtomicInteger
88
data class ActionTask(
99
val action: RestAction<*>,
1010
val description: String,
11-
val onSuccess: ((Any) -> Unit)? = null
11+
val onSuccess: ((Any?) -> Unit)? = null
1212
)
1313

1414
/**
@@ -32,7 +32,7 @@ fun Collection<ActionTask>.runActions(
3232
val range = endPercent - startPercent
3333

3434
for (task in this) {
35-
task.action.queue({ success: Any ->
35+
task.action.queue({ success: Any? ->
3636
statusLogger?.invoke(" - [完成] " + task.description)
3737
task.onSuccess?.invoke(success)
3838
handleTaskCompletion(completed, total, allDone, progressCallback, startPercent, range)

0 commit comments

Comments
 (0)