@@ -9,6 +9,8 @@ import MenuItem from '@mui/material/MenuItem'
99import Divider from '@mui/material/Divider'
1010import Collapse from '@mui/material/Collapse'
1111import Typography from '@mui/material/Typography'
12+ import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'
13+ import FavoriteIcon from '@mui/icons-material/Favorite'
1214import { useTranslation } from 'react-i18next'
1315
1416import { 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