Skip to content

Commit

Permalink
Add Preview Image to GameCard and LeaderBaseCard (#117)
Browse files Browse the repository at this point in the history
Updates `LeaderBaseCard` and `GameCard` to include a preview on hover,
using MUI Popover.

Handles a couple of instances where we might not want a preview:

- the Leader "box" if the leader has been deployed,
- the hidden cards in the opponents hand.

---------

Co-authored-by: dquilter <[email protected]>
Co-authored-by: Dan Bastin <[email protected]>
  • Loading branch information
3 people authored Feb 23, 2025
1 parent d0cd657 commit 21a6c0d
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 8 deletions.
3 changes: 2 additions & 1 deletion src/app/_components/_sharedcomponents/Cards/CardTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface ICardData {
zone?: string;
epicActionSpent?: boolean;
onStartingSide?: boolean;
controlled: boolean;
}

export interface IServerCardData {
Expand Down Expand Up @@ -108,4 +109,4 @@ interface ICardPlayer {
name: string;
label: string;
uuid: string;
}
}
81 changes: 78 additions & 3 deletions src/app/_components/_sharedcomponents/Cards/GameCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import {
Typography,
Box,
Popover,
PopoverOrigin,
} from '@mui/material';
import Grid from '@mui/material/Grid2';
import { IGameCardProps, ICardData, CardStyle } from './CardTypes';
Expand All @@ -20,6 +22,51 @@ const GameCard: React.FC<IGameCardProps> = ({
}) => {
const { sendGameMessage, connectedPlayer, getConnectedPlayerPrompt, distributionPromptData } = useGame();

const cardInPlayersHand = card.controller?.id === connectedPlayer && card.zone === 'hand';
const cardInOpponentsHand = card.controller?.id !== connectedPlayer && card.zone === 'hand';

const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(null);
const hoverTimeout = React.useRef<number | undefined>(undefined);
const open = Boolean(anchorElement);

const handlePreviewOpen = (event: React.MouseEvent<HTMLElement>) => {
const target = event.currentTarget;
if (cardInOpponentsHand) {
return;
}
hoverTimeout.current = window.setTimeout(() => {
setAnchorElement(target);
}, 500);
};

const handlePreviewClose = () => {
clearTimeout(hoverTimeout.current);
setAnchorElement(null);
};

const popoverConfig = (): { anchorOrigin: PopoverOrigin, transformOrigin: PopoverOrigin } => {
if (cardInPlayersHand) {
return {
anchorOrigin:{
vertical: -5,
horizontal: 'center',
},
transformOrigin: {
vertical: 'bottom',
horizontal: 'center',
} };
}

return {
anchorOrigin:{
vertical: 'center',
horizontal: -5,
},
transformOrigin: {
vertical: 'center',
horizontal: 'right',
} };
}

const showValueAdjuster = getConnectedPlayerPrompt()?.promptType === 'distributeAmongTargets' && card.selectable;
if (showValueAdjuster) {
Expand Down Expand Up @@ -251,12 +298,27 @@ const GameCard: React.FC<IGameCardProps> = ({
backgroundColor:'black',
mb:'0px',
position:'relative'
}
},
cardPreview: {
borderRadius: '.38em',
backgroundImage: `url(${s3CardImageURL(card)})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
aspectRatio: '1 / 1.4',
width: '16rem',
},
}

return (
<Box sx={styles.cardContainer}>
<Box sx={styles.card} onClick={disabled ? undefined : handleClick}>
<Box
sx={styles.card}
onClick={disabled ? undefined : handleClick}
aria-owns={open ? 'mouse-over-popover' : undefined}
aria-haspopup="true"
onMouseEnter={handlePreviewOpen}
onMouseLeave={handlePreviewClose}
>
<Box sx={styles.cardOverlay}>
<Box sx={styles.unimplementedAlert}></Box>
{ !!distributionAmount && (
Expand Down Expand Up @@ -302,6 +364,19 @@ const GameCard: React.FC<IGameCardProps> = ({
)}
</Box>

<Popover
id="mouse-over-popover"
sx={{ pointerEvents: 'none' }}
open={open}
anchorEl={anchorElement}
onClose={handlePreviewClose}
disableRestoreFocus
slotProps={{ paper: { sx: { backgroundColor: 'transparent' } } }}
{...popoverConfig()}
>
<Box sx={styles.cardPreview} />
</Popover>

{otherUpgradeCards.map((subcard) => (
<Box
key={subcard.uuid}
Expand Down Expand Up @@ -341,4 +416,4 @@ const GameCard: React.FC<IGameCardProps> = ({
);
};

export default GameCard;
export default GameCard;
67 changes: 63 additions & 4 deletions src/app/_components/_sharedcomponents/Cards/LeaderBaseCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import {
Typography,
Box
Box,
Popover
} from '@mui/material';
import { ILeaderBaseCardProps, LeaderBaseCardStyle } from './CardTypes';
import { useGame } from '@/app/_contexts/Game.context';
Expand All @@ -17,10 +18,28 @@ const LeaderBaseCard: React.FC<ILeaderBaseCardProps> = ({
disabled = false,
}) => {
const { sendGameMessage, connectedPlayer, getConnectedPlayerPrompt, distributionPromptData } = useGame();


const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(null);
const hoverTimeout = React.useRef<number | undefined>(undefined);
const open = Boolean(anchorElement);

if (!card) {
return null
}

const handlePreviewOpen = (event: React.MouseEvent<HTMLElement>) => {
const target = event.currentTarget;
if (isDeployed) return;
hoverTimeout.current = window.setTimeout(() => {
setAnchorElement(target);
}, 500);
};

const handlePreviewClose = () => {
clearTimeout(hoverTimeout.current);
setAnchorElement(null);
};

const isDeployed = card.hasOwnProperty('zone') && card.zone !== 'base';
const borderColor = getBorderColor(card, connectedPlayer, getConnectedPlayerPrompt()?.promptType);
const distributionAmount = distributionPromptData?.valueDistribution.find((item) => item.uuid === card.uuid)?.amount || 0;
Expand Down Expand Up @@ -122,7 +141,23 @@ const LeaderBaseCard: React.FC<ILeaderBaseCardProps> = ({
color: 'white',
fontWeight: '600',
fontSize: '1em',
}
},
cardPreview: {
borderRadius: '.38em',
backgroundImage: `url(${s3CardImageURL(card)})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
aspectRatio: '1.4 / 1',
width: '21rem',
},
cardPreviewDeployed: {
borderRadius: '.38em',
backgroundImage: `url(${s3CardImageURL(card)})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
aspectRatio: '1 / 1.4',
width: '16rem',
},
}

return (
Expand All @@ -133,6 +168,10 @@ const LeaderBaseCard: React.FC<ILeaderBaseCardProps> = ({
sendGameMessage(['cardClicked', card.uuid]);
}
}}
aria-owns={open ? 'mouse-over-popover' : undefined}
aria-haspopup="true"
onMouseEnter={handlePreviewOpen}
onMouseLeave={handlePreviewClose}
>
<Box sx={styles.cardOverlay}>
<Box sx={styles.unimplementedAlert}></Box>
Expand All @@ -152,6 +191,26 @@ const LeaderBaseCard: React.FC<ILeaderBaseCardProps> = ({
</Box>
)}

<Popover
id="mouse-over-popover"
sx={{ pointerEvents: 'none' }}
open={open}
anchorEl={anchorElement}
anchorOrigin={{
vertical: 'center',
horizontal: -5,
}}
transformOrigin={{
vertical: 'center',
horizontal: 'right',
}}
onClose={handlePreviewClose}
disableRestoreFocus
slotProps={{ paper: { sx: { backgroundColor: 'transparent' } } }}
>
<Box sx={styles.cardPreview} />
</Popover>

{cardStyle === LeaderBaseCardStyle.Leader && title && (
<>
<Box sx={styles.nameplateBox}>
Expand All @@ -165,4 +224,4 @@ const LeaderBaseCard: React.FC<ILeaderBaseCardProps> = ({
);
};

export default LeaderBaseCard;
export default LeaderBaseCard;

0 comments on commit 21a6c0d

Please sign in to comment.