Skip to content

Commit 0b51787

Browse files
committed
death flow enhancements
1 parent 65d6b8d commit 0b51787

12 files changed

Lines changed: 270 additions & 254 deletions

File tree

AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,22 @@ TypeScript/React frontend + JDA (Discord API) + MongoDB + WebSocket sync.
3737
- **Type Generation**: Run `yarn generate-api` after backend DTO changes to
3838
regenerate [types.gen.ts](src/dashboard/src/api/types.gen.ts)
3939

40+
### UI Layout Strategy
41+
42+
- **Sidebar-Centric Design**: The application uses a persistent `Sidebar` (
43+
`src/dashboard/src/components/layout/Sidebar.tsx`) as the primary game status hub.
44+
- Displays: Day count, game phase (Day/Night), step name, and countdown timer.
45+
- Controls: Contains navigation and status indicators (e.g., "Current Speaker").
46+
- **Responsiveness**: Key views (like `NightStatus`) use dynamic height calculations (`calc(100vh - ...)` ) and internal
47+
scrolling to ensure usability across mobile and desktop.
48+
4049
## Critical Patterns
4150

4251
### Data Model Hierarchy
4352

4453
- **Session** (guild-scoped): Contains `players: Map<String, Player>`, `currentState`, `stateData`, game settings
54+
- `stateData`: Stores transient but critical step data (e.g., `processedDeathPlayerIds` for robust death processing,
55+
poll results) to ensure resilience against restarts.
4556
- **Player** (nested class in Session): `roles: List<String>`, `alive: Boolean`, `police`, identity flags (`jinBaoBao`,
4657
`duplicated`)
4758
- **Game Steps**: State machine uses step IDs like "SETUP", "NIGHT", "SPEECH", "VOTING" - managed by `GameStateService`

src/dashboard/FEATURES.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ exhaustive way. It was assembled by statically scanning the dashboard source.
1818
- Back-to-login button
1919
- `/server/:guildId` (Main server dashboard)
2020
- `MainDashboard` (stage content + navigator)
21-
- `GameHeader` (day, step, speaker, timer, actions)
2221
- Stage-specific content (see Stages section)
2322
- `/server/:guildId/players` — Player management grid (`PlayerCard` list)
2423
- `/server/:guildId/speech``SpeechManager` (speech management UI)

src/dashboard/src/components/layout/Sidebar.tsx

Lines changed: 141 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
import { Activity, LayoutGrid, LogOut, Mic, Moon, Settings, Users } from 'lucide-react';
1+
import {
2+
Activity,
3+
LayoutGrid,
4+
LogOut,
5+
Mic,
6+
Moon,
7+
Settings,
8+
Settings2,
9+
Sun,
10+
Users,
11+
} from 'lucide-react';
212
import { useTranslation } from '@/lib/i18n';
313
import { ThemeToggle } from '@/components/ui/ThemeToggle';
414
import { useAuth } from '@/features/auth/contexts/AuthContext';
5-
import { useLocation } from 'react-router-dom';
15+
import { useLocation, useNavigate } from 'react-router-dom';
616
import { DiscordAvatar, DiscordName } from '@/components/DiscordUser';
17+
import { Player } from '@/api/types.gen';
18+
import { GAME_STEPS } from '@/features/game/constants';
719

820
interface SidebarProps {
921
onLogout: () => void;
@@ -16,6 +28,16 @@ interface SidebarProps {
1628
onToggleSpectatorMode: () => void;
1729
isSpectatorMode: boolean;
1830
isConnected: boolean;
31+
32+
// Game Header Relocated Props
33+
dayCount: number;
34+
timerSeconds: number;
35+
speech?: any;
36+
players?: Player[];
37+
currentStep?: string;
38+
currentState?: string;
39+
guildId?: string;
40+
isManualStep?: boolean;
1941
}
2042

2143
export const Sidebar: React.FC<SidebarProps> = ({
@@ -29,10 +51,19 @@ export const Sidebar: React.FC<SidebarProps> = ({
2951
onToggleSpectatorMode,
3052
isSpectatorMode,
3153
isConnected,
54+
dayCount,
55+
timerSeconds,
56+
speech,
57+
players,
58+
currentStep,
59+
currentState,
60+
guildId,
61+
isManualStep = false,
3262
}) => {
3363
const { t } = useTranslation();
3464
const { user } = useAuth();
3565
const location = useLocation();
66+
const navigate = useNavigate();
3667

3768
const isDashboardActive =
3869
location.pathname.endsWith(user?.user?.guildId ? `/server/${user.user.guildId}` : '/') &&
@@ -45,6 +76,15 @@ export const Sidebar: React.FC<SidebarProps> = ({
4576
const isSpectatorActive = location.pathname.includes('/spectator');
4677
const isSpeechActive = location.pathname.includes('/speech');
4778

79+
const currentSpeaker =
80+
speech?.currentSpeakerId && players
81+
? players.find((p) => p.id === speech.currentSpeakerId)
82+
: null;
83+
84+
const displayDay = dayCount === 0 && currentState !== 'SETUP' && !!currentState ? 1 : dayCount;
85+
const isNight = currentState?.includes('NIGHT');
86+
const isLobby = currentState === 'SETUP' || !currentState;
87+
4888
return (
4989
<aside className="w-full md:w-64 bg-slate-100 dark:bg-slate-950 border-r border-slate-300 dark:border-slate-800 flex flex-col shrink-0">
5090
<div className="p-6 flex items-center gap-3 border-b border-slate-300 dark:border-slate-800">
@@ -57,7 +97,42 @@ export const Sidebar: React.FC<SidebarProps> = ({
5797
</span>
5898
</div>
5999

60-
<nav className="flex-1 p-4 space-y-1">
100+
<div className="p-4 border-b border-slate-300 dark:border-slate-800 space-y-4">
101+
{/* Game Status */}
102+
<div className="bg-white dark:bg-slate-900 rounded-xl p-3 border border-slate-200 dark:border-slate-800 shadow-sm">
103+
<div className="flex items-center gap-2 mb-2 text-slate-900 dark:text-slate-100">
104+
{isLobby ? (
105+
<Settings2 className="w-4 h-4 text-slate-500 dark:text-slate-400" />
106+
) : isNight ? (
107+
<Moon className="w-4 h-4 text-indigo-500 dark:text-indigo-400" />
108+
) : (
109+
<Sun className="w-4 h-4 text-orange-500" />
110+
)}
111+
<span className="font-bold text-sm">
112+
{!isLobby && displayDay > 0 && t('game.day', { day: String(displayDay) }) + ' - '}
113+
{(() => {
114+
const stepInfo = GAME_STEPS.find((s) => s.id === currentStep);
115+
return stepInfo ? t(stepInfo.key) : currentStep || 'LOBBY';
116+
})()}
117+
</span>
118+
</div>
119+
120+
{!isManualStep && !isLobby && (
121+
<div className="flex items-center justify-between">
122+
<span className="text-[10px] text-slate-500 dark:text-slate-400 font-bold uppercase tracking-wider">
123+
{t('game.timer')}
124+
</span>
125+
<div
126+
className={`font-mono font-bold text-lg ${timerSeconds < 10 ? 'text-red-500 dark:text-red-400' : 'text-slate-800 dark:text-slate-200'}`}
127+
>
128+
{Math.floor(timerSeconds / 60)}:{String(timerSeconds % 60).padStart(2, '0')}
129+
</div>
130+
</div>
131+
)}
132+
</div>
133+
</div>
134+
135+
<nav className="flex-1 p-4 space-y-1 overflow-y-auto overflow-x-hidden">
61136
{user?.user?.role === 'JUDGE' && !isSpectatorMode && (
62137
<>
63138
<button
@@ -127,39 +202,70 @@ export const Sidebar: React.FC<SidebarProps> = ({
127202
<div className="p-4 border-t border-slate-300 dark:border-slate-800 space-y-3">
128203
{/* User Profile */}
129204
{user && (
130-
<div className="flex items-center gap-3 px-2 py-2 bg-slate-200/50 dark:bg-slate-800/50 rounded-lg">
131-
<DiscordAvatar
132-
userId={user.user.userId}
133-
guildId={user.user.guildId}
134-
avatarClassName="w-10 h-10 rounded-full ring-2 ring-indigo-200 dark:ring-indigo-900"
135-
/>
136-
<div className="flex-1 min-w-0">
137-
<p className="text-sm font-medium text-slate-900 dark:text-white truncate">
138-
<DiscordName userId={user.user.userId} guildId={user.user.guildId} />
139-
</p>
140-
{user.user.role === 'JUDGE' ? (
141-
<button
142-
onClick={onToggleSpectatorMode}
143-
className={`inline-block px-2 py-0.5 text-xs font-medium rounded transition-colors cursor-pointer ${
144-
isSpectatorMode
145-
? 'bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900'
146-
: 'bg-purple-100 dark:bg-purple-950 text-purple-700 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-900'
147-
}`}
148-
title={isSpectatorMode ? t('sidebar.backToJudge') : t('sidebar.viewAsSpectator')}
149-
>
150-
{isSpectatorMode ? t('userRoles.SPECTATOR') : t('userRoles.JUDGE')}
151-
</button>
152-
) : (
153-
<span
154-
className={`inline-block px-2 py-0.5 text-xs font-medium rounded ${
155-
user.user.role === 'SPECTATOR'
156-
? 'bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300'
157-
: 'bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-300'
158-
}`}
159-
>
160-
{t(`userRoles.${user.user.role}`) || user.user.role}
205+
<div className="space-y-3">
206+
{currentSpeaker && (
207+
<button
208+
onClick={() => navigate(`/server/${guildId}/speech`, { replace: true })}
209+
className="w-full flex flex-col items-center gap-1 p-3 bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-200 dark:border-indigo-800/50 rounded-xl transition-all hover:bg-indigo-100 dark:hover:bg-indigo-950/50 group"
210+
>
211+
<div className="flex items-center gap-2 text-indigo-600 dark:text-indigo-400">
212+
<Mic className="w-3 h-3 animate-pulse" />
213+
<span className="text-[10px] font-bold uppercase tracking-wider">
214+
{t('messages.speaking')}
215+
</span>
216+
</div>
217+
<span className="font-bold text-sm text-slate-900 dark:text-slate-100">
218+
{currentSpeaker.nickname}
161219
</span>
162-
)}
220+
{speech?.endTime && (
221+
<span className="text-xs font-mono text-indigo-600 dark:text-indigo-400 bg-indigo-100/50 dark:bg-indigo-900/30 px-2 py-0.5 rounded-full border border-indigo-200/50 dark:border-indigo-800/30 mt-1">
222+
{(() => {
223+
const seconds = Math.max(0, Math.ceil((speech.endTime - Date.now()) / 1000));
224+
return `${Math.floor(seconds / 60)
225+
.toString()
226+
.padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`;
227+
})()}
228+
</span>
229+
)}
230+
</button>
231+
)}
232+
233+
<div className="flex items-center gap-3 px-2 py-2 bg-slate-200/50 dark:bg-slate-800/50 rounded-lg">
234+
<DiscordAvatar
235+
userId={user.user.userId}
236+
guildId={user.user.guildId}
237+
avatarClassName="w-10 h-10 rounded-full ring-2 ring-indigo-200 dark:ring-indigo-900"
238+
/>
239+
<div className="flex-1 min-w-0">
240+
<p className="text-sm font-medium text-slate-900 dark:text-white truncate">
241+
<DiscordName userId={user.user.userId} guildId={user.user.guildId} />
242+
</p>
243+
{user.user.role === 'JUDGE' ? (
244+
<button
245+
onClick={onToggleSpectatorMode}
246+
className={`inline-block px-2 py-0.5 text-xs font-medium rounded transition-colors cursor-pointer ${
247+
isSpectatorMode
248+
? 'bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900'
249+
: 'bg-purple-100 dark:bg-purple-950 text-purple-700 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-900'
250+
}`}
251+
title={
252+
isSpectatorMode ? t('sidebar.backToJudge') : t('sidebar.viewAsSpectator')
253+
}
254+
>
255+
{isSpectatorMode ? t('userRoles.SPECTATOR') : t('userRoles.JUDGE')}
256+
</button>
257+
) : (
258+
<span
259+
className={`inline-block px-2 py-0.5 text-xs font-medium rounded ${
260+
user.user.role === 'SPECTATOR'
261+
? 'bg-blue-100 dark:bg-blue-950 text-blue-700 dark:text-blue-300'
262+
: 'bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-300'
263+
}`}
264+
>
265+
{t(`userRoles.${user.user.role}`) || user.user.role}
266+
</span>
267+
)}
268+
</div>
163269
</div>
164270
</div>
165271
)}

0 commit comments

Comments
 (0)