Skip to content

Commit 034bb09

Browse files
authored
[frontend] create ChallengeCard component (#2986)
Signed-off-by: Marine LM <[email protected]>
1 parent 5470739 commit 034bb09

File tree

4 files changed

+155
-318
lines changed

4 files changed

+155
-318
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { CrisisAlertOutlined, DescriptionOutlined, EmojiEventsOutlined, SportsScoreOutlined } from '@mui/icons-material';
2+
import {
3+
Avatar,
4+
Card,
5+
CardActions,
6+
CardContent,
7+
CardHeader,
8+
Typography,
9+
} from '@mui/material';
10+
import { type ReactNode } from 'react';
11+
import { makeStyles } from 'tss-react/mui';
12+
13+
import ExpandableMarkdown from '../../../../components/ExpandableMarkdown';
14+
import ItemTags from '../../../../components/ItemTags';
15+
import type { Challenge } from '../../../../utils/api-types';
16+
17+
const useStyles = makeStyles()(theme => ({
18+
cardContainer: {
19+
display: 'flex',
20+
flexDirection: 'column',
21+
},
22+
iconInfo: {
23+
display: 'flex',
24+
alignItems: 'center',
25+
marginTop: 'auto',
26+
},
27+
marginRight2: { marginRight: theme.spacing(2) },
28+
cardClickable: {
29+
'cursor': 'pointer',
30+
'&:hover': { backgroundColor: theme.palette.action.hover },
31+
},
32+
}));
33+
34+
interface Props {
35+
challenge: Challenge;
36+
showTags?: boolean;
37+
clickable?: boolean;
38+
onClick?: () => void;
39+
actionHeader?: ReactNode;
40+
}
41+
42+
const ChallengeCard = ({ challenge, showTags = false, clickable = false, onClick, actionHeader }: Props) => {
43+
const { classes } = useStyles();
44+
const onCardClick = () => {
45+
if (clickable && onClick) {
46+
onClick();
47+
}
48+
};
49+
return (
50+
<Card
51+
variant="outlined"
52+
onClick={onCardClick}
53+
className={`${classes.cardContainer} ${clickable ? classes.cardClickable : ''}`}
54+
>
55+
<CardHeader
56+
avatar={(
57+
<Avatar sx={{ backgroundColor: '#e91e63' }} aria-label="challenge-icon">
58+
<EmojiEventsOutlined />
59+
</Avatar>
60+
)}
61+
title={challenge.challenge_name}
62+
subheader={challenge.challenge_category}
63+
action={actionHeader}
64+
/>
65+
<CardContent>
66+
<ExpandableMarkdown
67+
source={challenge.challenge_content}
68+
limit={500}
69+
/>
70+
</CardContent>
71+
<CardActions classes={{ root: classes.iconInfo }}>
72+
{showTags && (challenge.challenge_tags?.length ?? 0) > 0 && <ItemTags variant="list" tags={challenge.challenge_tags} />}
73+
74+
<SportsScoreOutlined style={{ marginLeft: 'auto' }} fontSize="small" color="primary" />
75+
<Typography classes={{ root: classes.marginRight2 }} color="primary" variant="body2">{challenge.challenge_score ?? 0}</Typography>
76+
<CrisisAlertOutlined fontSize="small" color="primary" />
77+
<Typography classes={{ root: classes.marginRight2 }} color="primary" variant="body2">{challenge.challenge_max_attempts ?? 0}</Typography>
78+
<DescriptionOutlined fontSize="small" color="primary" />
79+
<Typography classes={{ root: classes.marginRight2 }} color="primary" variant="body2">{challenge.challenge_documents?.length ?? 0}</Typography>
80+
</CardActions>
81+
</Card>
82+
);
83+
};
84+
85+
export default ChallengeCard;

openbas-front/src/admin/components/common/challenges/ContextualChallenges.js

+12-82
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import {
2-
CrisisAlertOutlined,
3-
DescriptionOutlined,
4-
EmojiEventsOutlined,
5-
OutlinedFlagOutlined,
62
SlowMotionVideoOutlined,
7-
SportsScoreOutlined,
83
VisibilityOutlined,
94
} from '@mui/icons-material';
10-
import { Avatar, Button, Card, CardContent, CardHeader, Chip, GridLegacy, IconButton, Tooltip } from '@mui/material';
5+
import { Button, IconButton, Tooltip } from '@mui/material';
6+
import { useTheme } from '@mui/material/styles';
117
import { useContext } from 'react';
128
import { Link } from 'react-router';
139
import { makeStyles } from 'tss-react/mui';
1410

1511
import Empty from '../../../../components/Empty';
16-
import ExpandableMarkdown from '../../../../components/ExpandableMarkdown';
1712
import { useFormatter } from '../../../../components/i18n';
1813
import useSearchAnFilter from '../../../../utils/SortingFiltering';
1914
import { ChallengeContext } from '../Context';
15+
import ChallengeCard from './ChallengeCard.js';
2016

2117
const useStyles = makeStyles()(() => ({
2218
flag: {
@@ -45,6 +41,7 @@ const ContextualChallenges = ({ challenges, linkToInjects }) => {
4541
// Standard hooks
4642
const { classes } = useStyles();
4743
const { t } = useFormatter();
44+
const theme = useTheme();
4845

4946
// Context
5047
const { previewChallengeUrl } = useContext(ChallengeContext);
@@ -95,81 +92,14 @@ const ContextualChallenges = ({ challenges, linkToInjects }) => {
9592
)}
9693
/>
9794
)}
98-
<GridLegacy container={true} spacing={3}>
99-
{sortedChallenges.map((challenge, index) => {
100-
return (
101-
<GridLegacy key={challenge.challenge_id} item={true} xs={4} style={index < 3 ? { paddingTop: 0 } : undefined}>
102-
<Card
103-
variant="outlined"
104-
classes={{ root: classes.card }}
105-
sx={{
106-
width: '100%',
107-
height: '100%',
108-
}}
109-
>
110-
<CardHeader
111-
avatar={(
112-
<Avatar sx={{ bgcolor: '#e91e63' }}>
113-
<EmojiEventsOutlined />
114-
</Avatar>
115-
)}
116-
title={challenge.challenge_name}
117-
subheader={challenge.challenge_category}
118-
/>
119-
<CardContent style={{ margin: '-20px 0 30px 0' }}>
120-
<ExpandableMarkdown
121-
source={challenge.challenge_content}
122-
limit={500}
123-
controlled={true}
124-
/>
125-
<div className={classes.footer}>
126-
<div style={{ float: 'left' }}>
127-
{challenge.challenge_flags.map((flag) => {
128-
return (
129-
<Tooltip
130-
key={flag.flag_id}
131-
title={t(flag.flag_type)}
132-
>
133-
<Chip
134-
icon={<OutlinedFlagOutlined />}
135-
classes={{ root: classes.flag }}
136-
variant="outlined"
137-
label={t(flag.flag_type)}
138-
/>
139-
</Tooltip>
140-
);
141-
})}
142-
</div>
143-
<div style={{ float: 'right' }}>
144-
<Button
145-
size="small"
146-
startIcon={<SportsScoreOutlined />}
147-
className={classes.button}
148-
>
149-
{challenge.challenge_score || 0}
150-
</Button>
151-
<Button
152-
size="small"
153-
startIcon={<CrisisAlertOutlined />}
154-
className={classes.button}
155-
>
156-
{challenge.challenge_max_attempts || 0}
157-
</Button>
158-
<Button
159-
size="small"
160-
startIcon={<DescriptionOutlined />}
161-
className={classes.button}
162-
>
163-
{challenge.challenge_documents.length || 0}
164-
</Button>
165-
</div>
166-
</div>
167-
</CardContent>
168-
</Card>
169-
</GridLegacy>
170-
);
171-
})}
172-
</GridLegacy>
95+
<div style={{
96+
display: 'grid',
97+
gridTemplateColumns: '1fr 1fr 1fr',
98+
gap: theme.spacing(3),
99+
}}
100+
>
101+
{sortedChallenges.map(challenge => <ChallengeCard showTags key={challenge.challenge_id} challenge={challenge} />)}
102+
</div>
173103
</>
174104
);
175105
};

0 commit comments

Comments
 (0)