Skip to content

Commit bedf5b3

Browse files
committed
magician fixes (half tested)
1 parent 4235c7d commit bedf5b3

12 files changed

Lines changed: 793 additions & 411 deletions

File tree

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

Lines changed: 72 additions & 356 deletions
Large diffs are not rendered by default.
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import React from 'react';
2+
import { useTranslation } from '@/lib/i18n';
3+
import { ArrowLeftRight, Check, Brain } from 'lucide-react';
4+
import { Player, RoleActionInstance } from '@/api/types.gen';
5+
import { getRoleConfig } from '@/constants/gameData';
6+
import { DiscordAvatar, DiscordName } from '@/components/DiscordUser';
7+
import { EnrichedActionStatus } from './types';
8+
9+
interface MagicianActionCardProps {
10+
status: EnrichedActionStatus | RoleActionInstance;
11+
index?: number;
12+
players: Player[];
13+
guildId?: string;
14+
variant?: 'default' | 'large';
15+
}
16+
17+
export const MagicianActionCard: React.FC<MagicianActionCardProps> = ({
18+
status,
19+
index = 0,
20+
players,
21+
guildId,
22+
variant = 'default',
23+
}) => {
24+
const { t } = useTranslation();
25+
26+
// Handle potentially different shape of status object
27+
const actorRole = 'actorRole' in status ? status.actorRole : 'MAGICIAN';
28+
const roleConfig = getRoleConfig(actorRole);
29+
const RoleIcon = roleConfig.icon || Brain;
30+
const roleName = t('roles.magician');
31+
32+
const isActing = status.status === 'ACTING';
33+
const isSkipped = status.status === 'SKIPPED';
34+
const isProcessed = status.status === 'PROCESSED';
35+
const isSubmitted = status.status === 'SUBMITTED' || isProcessed;
36+
37+
const t1Id = status.targets?.[0];
38+
const t2Id = status.targets?.[1];
39+
const t1 = t1Id && t1Id !== -1 ? players.find((p) => p.id === Number(t1Id)) : null;
40+
const t2 = t2Id && t2Id !== -1 ? players.find((p) => p.id === Number(t2Id)) : null;
41+
42+
const actorId = 'playerUserId' in status ? status.playerUserId : (status as RoleActionInstance).actor;
43+
const actor = players.find(p => p.id === Number(actorId));
44+
45+
const isLarge = variant === 'large';
46+
47+
// --- Sub-components ---
48+
49+
const TargetDisplay = ({ target, label }: { target: Player | null | undefined, label: string }) => (
50+
<div className={`flex flex-col items-center gap-2 ${isLarge ? 'z-10' : 'flex-1'}`}>
51+
<div
52+
className={`
53+
rounded-full p-1 bg-white dark:bg-slate-800 relative
54+
${isLarge
55+
? 'w-24 h-24 shadow-xl ring-4 ring-slate-100 dark:ring-slate-800'
56+
: `w-10 h-10 border-2 overflow-hidden flex items-center justify-center ${target ? 'border-indigo-400' : 'border-slate-300 dark:border-slate-700 dashed'}`
57+
}
58+
`}
59+
>
60+
{target ? (
61+
<div className="w-full h-full rounded-full overflow-hidden relative">
62+
<DiscordAvatar
63+
userId={String(target.userId)}
64+
guildId={guildId}
65+
avatarClassName="w-full h-full object-cover"
66+
/>
67+
</div>
68+
) : (
69+
<div className="w-full h-full rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
70+
<div className={`${isLarge ? 'text-4xl' : 'text-xs'} text-slate-300 dark:text-slate-600 font-thin`}>
71+
?
72+
</div>
73+
</div>
74+
)}
75+
{isLarge && (
76+
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 bg-slate-800 text-white text-[10px] font-bold px-2 py-0.5 rounded-full uppercase">
77+
{label}
78+
</div>
79+
)}
80+
</div>
81+
<div className="text-center">
82+
{isLarge ? (
83+
<p className="font-bold text-slate-900 dark:text-white text-lg">
84+
{target ? (
85+
<DiscordName
86+
userId={String(target.userId)}
87+
guildId={guildId}
88+
fallbackName={target.nickname}
89+
/>
90+
) : (
91+
<span className="text-slate-400 italic">
92+
{t('nightStatus.waiting')}
93+
</span>
94+
)}
95+
</p>
96+
) : (
97+
<span className="text-[10px] font-bold text-slate-600 dark:text-slate-300 truncate max-w-[60px] block">
98+
{target ? (
99+
<DiscordName
100+
userId={String(target.userId)}
101+
guildId={guildId}
102+
fallbackName={target.nickname}
103+
/>
104+
) : (
105+
t('nightStatus.waiting')
106+
)}
107+
</span>
108+
)}
109+
</div>
110+
</div>
111+
);
112+
113+
const StatusBadge = () => (
114+
<div
115+
className={`
116+
px-2.5 py-1 rounded-full text-[10px] font-bold tracking-wide uppercase border flex items-center gap-1
117+
${isLarge ? 'px-4 py-1.5 text-sm rounded-full tracking-wider' : 'rounded'}
118+
${
119+
isSubmitted
120+
? 'bg-green-100 text-green-700 border-green-200 dark:bg-green-500/10 dark:text-green-400 dark:border-green-500/30'
121+
: isActing
122+
? 'bg-purple-500/10 text-purple-500 border-purple-500/20 animate-pulse'
123+
: 'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/30'
124+
}
125+
`}
126+
>
127+
{isProcessed && <Check className="w-3 h-3" />}
128+
{status.status
129+
? t(`nightStatus.${status.status.toLowerCase()}`)
130+
: status.status}
131+
</div>
132+
);
133+
134+
const SwapIcon = () => (
135+
<div className={`flex items-center justify-center ${isLarge ? 'z-10 bg-white dark:bg-slate-900 p-4 rounded-full shadow-lg border border-slate-100 dark:border-slate-800' : 'px-2'}`}>
136+
{isLarge ? (
137+
<ArrowLeftRight className="w-8 h-8 text-purple-500" />
138+
) : (
139+
<div className="w-8 h-8 rounded-full bg-white dark:bg-slate-800 shadow-sm border border-slate-200 dark:border-slate-700 flex items-center justify-center">
140+
<ArrowLeftRight className="w-4 h-4 text-purple-500" />
141+
</div>
142+
)}
143+
</div>
144+
);
145+
146+
const PerformerInfo = () => (
147+
<div className={`flex items-center justify-end gap-2 opacity-60 ${isLarge ? 'mt-12 justify-center' : 'mt-3'}`}>
148+
{isLarge ? (
149+
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 bg-slate-50 dark:bg-slate-800/50 px-3 py-1.5 rounded-full">
150+
<span className="uppercase text-[10px] font-bold tracking-wider">{t('nightStatus.actor')}:</span>
151+
<span>{actor?.nickname || 'Unknown'}</span>
152+
<DiscordAvatar userId={String(actor?.userId)} guildId={guildId} avatarClassName="w-4 h-4 rounded-full" />
153+
</div>
154+
) : (
155+
<>
156+
<span className="text-[9px] uppercase font-bold text-slate-400">{t('nightStatus.actor')}</span>
157+
<div className="w-5 h-5 rounded-full border border-slate-200 dark:border-slate-700 overflow-hidden">
158+
<DiscordAvatar userId={String(actorId)} guildId={guildId} avatarClassName="w-full h-full object-cover" />
159+
</div>
160+
</>
161+
)}
162+
</div>
163+
);
164+
165+
const Header = () => (
166+
<div className={isLarge ? "flex flex-col md:flex-row items-center justify-between gap-6 mb-12" : "flex justify-between items-start mb-6"}>
167+
<div className={`flex items-center ${isLarge ? 'gap-5' : 'gap-3'}`}>
168+
<div
169+
className={`flex items-center justify-center ${
170+
isLarge
171+
? `p-4 rounded-2xl shadow-lg`
172+
: `h-10 w-10 rounded-lg`
173+
}`}
174+
style={{
175+
backgroundColor: isSkipped ? undefined : `${roleConfig.color}20`,
176+
color: isSkipped ? undefined : roleConfig.color,
177+
}}
178+
>
179+
<RoleIcon className={isLarge ? "w-8 h-8" : "w-5 h-5"} />
180+
</div>
181+
<div>
182+
<h4 className={`font-bold text-slate-900 dark:text-white ${isLarge ? 'text-3xl tracking-tight' : ''}`}>
183+
{isLarge ? roleName : actorRole}
184+
</h4>
185+
<span
186+
className={
187+
isLarge
188+
? "text-slate-500 dark:text-slate-400 font-medium"
189+
: `text-[10px] font-bold mt-1 px-1.5 py-0.5 rounded inline-block`
190+
}
191+
style={!isLarge ? {
192+
backgroundColor: `${roleConfig.color}20`,
193+
color: roleConfig.color,
194+
} : undefined}
195+
>
196+
{t('actions.labels.MAGICIAN_SWAP') || 'Swap'}
197+
</span>
198+
</div>
199+
</div>
200+
<StatusBadge />
201+
</div>
202+
);
203+
204+
const Body = () => (
205+
<div className={
206+
isLarge
207+
? "flex flex-col md:flex-row items-center justify-center gap-8 md:gap-12 relative"
208+
: "bg-slate-50 dark:bg-black/20 rounded-lg p-4 relative min-h-[100px] border border-slate-100 dark:border-slate-700 flex items-center justify-between"
209+
}>
210+
<TargetDisplay target={t1} label={t('nightStatus.target') + " 1"} />
211+
<SwapIcon />
212+
<TargetDisplay target={t2} label={t('nightStatus.target') + " 2"} />
213+
</div>
214+
);
215+
216+
// --- Render ---
217+
218+
const content = (
219+
<>
220+
<Header />
221+
<Body />
222+
<PerformerInfo />
223+
</>
224+
);
225+
226+
if (isLarge) {
227+
return (
228+
<div className="max-w-4xl mx-auto mt-10 animate-in fade-in zoom-in-95 duration-500 p-4">
229+
<div
230+
className="relative overflow-hidden rounded-3xl shadow-2xl bg-white dark:bg-slate-900 border-2"
231+
style={{ borderColor: roleConfig.color }}
232+
>
233+
<div
234+
className="absolute top-0 left-0 right-0 h-32 opacity-10"
235+
style={{
236+
background: `linear-gradient(to bottom, ${roleConfig.color}, transparent)`,
237+
}}
238+
/>
239+
240+
<div className="relative p-8 md:p-12">
241+
{content}
242+
</div>
243+
</div>
244+
</div>
245+
);
246+
}
247+
248+
return (
249+
<div
250+
key={`${'actor' in status ? status.actor : (status as any).actor}-${index}`}
251+
className={`bg-white dark:bg-slate-900 rounded-xl border-l-4 border-t border-r border-b border-slate-200 dark:border-slate-800 overflow-hidden group transition-all duration-300 animate-in fade-in slide-in-from-left-4 fill-mode-both ${
252+
isActing ? 'ring-1' : 'shadow-lg'
253+
} ${isSkipped ? 'opacity-75 grayscale border-l-slate-500' : ''}`}
254+
style={{
255+
animationDelay: `${250 + index * 100}ms`,
256+
borderLeftColor: isSkipped ? undefined : roleConfig.color,
257+
boxShadow: isActing ? `0 0 20px ${roleConfig.color}40` : undefined,
258+
}}
259+
>
260+
<div className="p-5 relative z-10">
261+
{content}
262+
</div>
263+
</div>
264+
);
265+
};

0 commit comments

Comments
 (0)