Skip to content

Commit ad4a7ca

Browse files
author
Zyki
committed
feat: front-end display gym defenders
1 parent 854b3e7 commit ad4a7ca

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed

src/features/gym/GymPopup.jsx

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import MenuItem from '@mui/material/MenuItem'
99
import Divider from '@mui/material/Divider'
1010
import Collapse from '@mui/material/Collapse'
1111
import Typography from '@mui/material/Typography'
12+
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'
13+
import FavoriteIcon from '@mui/icons-material/Favorite'
1214
import { useTranslation } from 'react-i18next'
1315

1416
import { useSyncData } from '@features/webhooks'
@@ -44,9 +46,27 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) {
4446
const { perms } = useMemory((s) => s.auth)
4547
const popups = useStorage((s) => s.popups)
4648
const ts = Math.floor(Date.now() / 1000)
49+
const [showDefenders, setShowDefenders] = React.useState(false)
4750

4851
useAnalytics('Popup', `Team ID: ${gym.team_id} Has Raid: ${hasRaid}`, 'Gym')
4952

53+
// If defenders modal is toggled, show only that
54+
if (showDefenders) {
55+
return (
56+
<ErrorBoundary noRefresh style={{}} variant="h5">
57+
<Grid
58+
container
59+
direction="row"
60+
justifyContent="center"
61+
alignItems="center"
62+
width={200}
63+
>
64+
<DefendersModal gym={gym} onClose={() => setShowDefenders(false)} />
65+
</Grid>
66+
</ErrorBoundary>
67+
)
68+
}
69+
5070
return (
5171
<ErrorBoundary noRefresh style={{}} variant="h5">
5272
<Grid
@@ -60,6 +80,28 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) {
6080
<Title backup={t('unknown_gym')}>{gym.name}</Title>
6181
</Grid>
6282
<MenuActions hasRaid={hasRaid} {...gym} />
83+
{gym.defenders?.length > 0 && (
84+
<Grid xs={12} textAlign="center" my={1}>
85+
<button
86+
type="button"
87+
style={{
88+
padding: 6,
89+
borderRadius: 8,
90+
border: '1px solid #ccc',
91+
background: '#fff',
92+
fontWeight: 600,
93+
width: '100%',
94+
fontSize: 14,
95+
}}
96+
onClick={(e) => {
97+
e.stopPropagation()
98+
setShowDefenders(true)
99+
}}
100+
>
101+
{t('view_defenders')}
102+
</button>
103+
</Grid>
104+
)}
63105
{perms.gyms && (
64106
<Grid xs={12}>
65107
<Collapse
@@ -116,6 +158,217 @@ export function GymPopup({ hasRaid, hasHatched, raidIconUrl, ...gym }) {
116158
)
117159
}
118160

161+
/**
162+
* Compact modal for gym defenders
163+
* @param {{ gym: import('@rm/types').Gym, onClose: () => void }} param0
164+
*/
165+
function DefendersModal({ gym, onClose }) {
166+
const { t } = useTranslation()
167+
const Icons = useMemory((s) => s.Icons)
168+
const defenders = gym.defenders || []
169+
170+
return (
171+
<Grid
172+
container
173+
direction="column"
174+
alignItems="stretch"
175+
style={{ minWidth: 250, maxWidth: 350, padding: 8 }}
176+
>
177+
<Grid container alignItems="center" mb={1}>
178+
<Grid xs={2}>
179+
<IconButton
180+
onClick={(e) => {
181+
e.stopPropagation()
182+
onClose()
183+
}}
184+
size="small"
185+
>
186+
<ArrowBackIosNewIcon fontSize="small" />
187+
</IconButton>
188+
</Grid>
189+
<Grid
190+
xs={8}
191+
style={{
192+
overflow: 'hidden',
193+
textOverflow: 'ellipsis',
194+
wordBreak: 'break-word',
195+
maxWidth: 200,
196+
display: 'flex',
197+
alignItems: 'center',
198+
}}
199+
>
200+
<Title backup={t('unknown_gym')}>{gym.name}</Title>
201+
</Grid>
202+
</Grid>
203+
<Grid container direction="column" spacing={1}>
204+
{defenders.map((def) => {
205+
const fullCP = def.cp_when_deployed
206+
const currentCP = def.cp_now
207+
const percent = Math.max(0, Math.min(1, currentCP / fullCP))
208+
209+
return (
210+
<div
211+
key={def.pokemon_id}
212+
style={{
213+
display: 'flex',
214+
alignItems: 'center',
215+
minHeight: 60,
216+
width: '100%',
217+
padding: '4px 0',
218+
}}
219+
>
220+
<div
221+
style={{
222+
marginLeft: 8,
223+
marginRight: 8,
224+
display: 'flex',
225+
alignItems: 'center',
226+
flexShrink: 0,
227+
}}
228+
>
229+
<Img
230+
src={Icons.getPokemonByDisplay(def.pokemon_id, def)}
231+
alt={t(`poke_${def.pokemon_id}`)}
232+
maxHeight={44}
233+
maxWidth={44}
234+
style={{ objectFit: 'contain' }}
235+
/>
236+
</div>
237+
<div
238+
style={{
239+
flex: 1,
240+
display: 'flex',
241+
flexDirection: 'column',
242+
alignItems: 'flex-start',
243+
justifyContent: 'center',
244+
minWidth: 0,
245+
textAlign: 'left',
246+
overflow: 'hidden',
247+
marginLeft: 4,
248+
}}
249+
>
250+
<span
251+
style={{
252+
fontSize: 15,
253+
fontWeight: 600,
254+
marginBottom: 2,
255+
overflow: 'hidden',
256+
textOverflow: 'ellipsis',
257+
whiteSpace: 'nowrap',
258+
maxWidth: '100%',
259+
}}
260+
title={t(`poke_${def.pokemon_id}`)}
261+
>
262+
{t(`poke_${def.pokemon_id}`)}
263+
</span>
264+
<span style={{ fontSize: 13, color: '#666' }}>
265+
CP: <b>{currentCP}</b> / {fullCP}
266+
</span>
267+
</div>
268+
<div
269+
style={{
270+
width: 44,
271+
minWidth: 44,
272+
maxWidth: 44,
273+
height: 44,
274+
display: 'flex',
275+
alignItems: 'center',
276+
justifyContent: 'flex-end',
277+
position: 'relative',
278+
marginLeft: 4,
279+
marginRight: 8,
280+
flexShrink: 0,
281+
}}
282+
>
283+
{/* Heart outline */}
284+
<FavoriteIcon
285+
style={{
286+
color: 'transparent',
287+
position: 'absolute',
288+
top: 0,
289+
right: 0,
290+
width: 28,
291+
height: 28,
292+
stroke: 'white',
293+
strokeWidth: 1,
294+
filter: 'drop-shadow(0 0 1px #0008)',
295+
}}
296+
className="heart-outline"
297+
/>
298+
{/* Heart background */}
299+
<FavoriteIcon
300+
style={{
301+
color: 'white',
302+
opacity: 0.18,
303+
position: 'absolute',
304+
top: 0,
305+
right: 0,
306+
width: 28,
307+
height: 28,
308+
}}
309+
/>
310+
{/* Heart fill */}
311+
<FavoriteIcon
312+
style={{
313+
color: '#ff69b4',
314+
position: 'absolute',
315+
top: 0,
316+
right: 0,
317+
width: 28,
318+
height: 28,
319+
clipPath: `inset(${100 - percent * 100}% 0 0 0)`,
320+
transition: 'clip-path 0.3s',
321+
}}
322+
/>
323+
{/* Heart cracks for rounds */}
324+
<svg
325+
width={28}
326+
height={28}
327+
viewBox="0 0 28 28"
328+
style={{
329+
position: 'absolute',
330+
top: 0,
331+
right: 0,
332+
pointerEvents: 'none',
333+
}}
334+
>
335+
{/* Crack at 1/3 height (top) */}
336+
<path
337+
d="M2,9 Q7,11 14,9 Q21,11 26,9"
338+
stroke="white"
339+
strokeWidth={1.5}
340+
fill="none"
341+
strokeLinejoin="round"
342+
/>
343+
{/* Crack at 2/3 height (bottom, improved to fit heart) */}
344+
<path
345+
d="M7,19 Q11,17 14,19 Q17,17 21,19"
346+
stroke="white"
347+
strokeWidth={1.5}
348+
fill="none"
349+
strokeLinejoin="round"
350+
/>
351+
</svg>
352+
</div>
353+
</div>
354+
)
355+
})}
356+
</Grid>
357+
<Grid
358+
xs={12}
359+
textAlign="center"
360+
mt={2}
361+
style={{ fontSize: 12, color: '#888' }}
362+
>
363+
{t('last_updated')}:{' '}
364+
{gym.updated
365+
? new Date(gym.updated * 1000).toLocaleString()
366+
: t('unknown')}
367+
</Grid>
368+
</Grid>
369+
)
370+
}
371+
119372
/**
120373
*
121374
* @param {{

0 commit comments

Comments
 (0)