@comicrelief/component-library 8.83.0 → 8.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +1237 -868
- package/dist/index.es.js +10978 -10219
- package/package.json +1 -1
- package/src/App.styles.js +2 -2
- package/src/components/Atoms/Button/Button.js +13 -3
- package/src/components/Atoms/Button/Button.style.js +19 -1
- package/src/components/Atoms/Button/Button.test.js +1 -1
- package/src/components/Atoms/Button/ButtonExample.jsx +24 -0
- package/src/components/Atoms/ButtonWithStates/ButtonWithStates.js +1 -1
- package/src/components/Atoms/Icons/Cross.js +0 -1
- package/src/components/Atoms/Icons/Download.js +4 -4
- package/src/components/Atoms/Icons/External.js +4 -4
- package/src/components/Atoms/Icons/Filter.js +50 -0
- package/src/components/Atoms/Icons/IconsExample.jsx +9 -5
- package/src/components/Atoms/Icons/Undo.js +44 -0
- package/src/components/Atoms/Icons/index.js +2 -0
- package/src/components/Atoms/Link/Link.test.js +1 -1
- package/src/components/Atoms/Link/LinkExample.jsx +4 -2
- package/src/components/Atoms/Text/TextExample.jsx +338 -130
- package/src/components/Molecules/CardDs/__snapshots__/CardDs.test.js.snap +2 -2
- package/src/components/Molecules/ExpandedButton/ExpandedButton.style.js +1 -0
- package/src/components/Molecules/ExpandedButton/__snapshots__/ExpandedButton.test.js.snap +1 -0
- package/src/components/Molecules/Promo/__snapshots__/Promo.test.js.snap +2 -2
- package/src/components/Molecules/QuoteSlice/__snapshots__/QuoteSlice.test.js.snap +1 -1
- package/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap +2 -2
- package/src/components/Molecules/SingleMessageDS/__snapshots__/SingleMessageDs.test.js.snap +1 -3
- package/src/components/Organisms/CookieBanner/CookieBanner.test.js +1 -1
- package/src/components/Organisms/EmailBanner/__snapshots__/EmailBanner.test.js.snap +12 -12
- package/src/components/Organisms/EmailSignUp/__snapshots__/EmailSignUp.test.js.snap +3 -3
- package/src/components/Organisms/FilterCard/FilterCard.js +286 -0
- package/src/components/Organisms/FilterCard/FilterCard.style.js +425 -0
- package/src/components/Organisms/FilterCard/FilterCard.test.js +60 -0
- package/src/components/Organisms/FilterCard/FilterCardExample.jsx +48 -0
- package/src/components/Organisms/FilterCard/FilterCardNodes.js +155 -0
- package/src/components/Organisms/FilterCard/__snapshots__/FilterCard.test.js.snap +1006 -0
- package/src/components/Organisms/FilterCard/_utils/_utils.js +57 -0
- package/src/components/Organisms/FilterCard/assets/Rectangle.svg +3 -0
- package/src/components/Organisms/FooterNew/__snapshots__/FooterNew.test.js.snap +4 -4
- package/src/components/Organisms/MarketingPreferencesDS/MarketingPreferencesDS.style.js +1 -1
- package/src/data/TestFilterCardNodeData.js +930 -0
- package/src/data/data.js +64 -1
- package/src/data/test-icons/Children--black.svg +5 -0
- package/src/data/test-icons/Children--white.svg +5 -0
- package/src/demos/index.jsx +31 -9
- package/src/index.js +1 -0
- package/src/theme/crTheme/buttonColors.js +3 -4
|
@@ -41,12 +41,12 @@ exports[`Image banner left orientation renders correctly 1`] = `
|
|
|
41
41
|
|
|
42
42
|
.c21:disabled {
|
|
43
43
|
cursor: not-allowed;
|
|
44
|
-
opacity: 0.
|
|
44
|
+
opacity: 0.35;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.c22:disabled {
|
|
48
48
|
cursor: not-allowed;
|
|
49
|
-
opacity: 0.
|
|
49
|
+
opacity: 0.35;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
.c6 {
|
|
@@ -621,7 +621,7 @@ exports[`Image banner left orientation renders correctly 1`] = `
|
|
|
621
621
|
<button
|
|
622
622
|
className="c21 c22"
|
|
623
623
|
data-test="subscribe-button"
|
|
624
|
-
data-testid="
|
|
624
|
+
data-testid="ButtonWithStates"
|
|
625
625
|
disabled={false}
|
|
626
626
|
type="submit"
|
|
627
627
|
>
|
|
@@ -681,12 +681,12 @@ exports[`Image banner renders correctly 1`] = `
|
|
|
681
681
|
|
|
682
682
|
.c21:disabled {
|
|
683
683
|
cursor: not-allowed;
|
|
684
|
-
opacity: 0.
|
|
684
|
+
opacity: 0.35;
|
|
685
685
|
}
|
|
686
686
|
|
|
687
687
|
.c22:disabled {
|
|
688
688
|
cursor: not-allowed;
|
|
689
|
-
opacity: 0.
|
|
689
|
+
opacity: 0.35;
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
.c6 {
|
|
@@ -1261,7 +1261,7 @@ exports[`Image banner renders correctly 1`] = `
|
|
|
1261
1261
|
<button
|
|
1262
1262
|
className="c21 c22"
|
|
1263
1263
|
data-test="subscribe-button"
|
|
1264
|
-
data-testid="
|
|
1264
|
+
data-testid="ButtonWithStates"
|
|
1265
1265
|
disabled={false}
|
|
1266
1266
|
type="submit"
|
|
1267
1267
|
>
|
|
@@ -1321,12 +1321,12 @@ exports[`Text-only email widget renders correctly 1`] = `
|
|
|
1321
1321
|
|
|
1322
1322
|
.c21:disabled {
|
|
1323
1323
|
cursor: not-allowed;
|
|
1324
|
-
opacity: 0.
|
|
1324
|
+
opacity: 0.35;
|
|
1325
1325
|
}
|
|
1326
1326
|
|
|
1327
1327
|
.c22:disabled {
|
|
1328
1328
|
cursor: not-allowed;
|
|
1329
|
-
opacity: 0.
|
|
1329
|
+
opacity: 0.35;
|
|
1330
1330
|
}
|
|
1331
1331
|
|
|
1332
1332
|
.c6 {
|
|
@@ -1901,7 +1901,7 @@ exports[`Text-only email widget renders correctly 1`] = `
|
|
|
1901
1901
|
<button
|
|
1902
1902
|
className="c21 c22"
|
|
1903
1903
|
data-test="subscribe-button"
|
|
1904
|
-
data-testid="
|
|
1904
|
+
data-testid="ButtonWithStates"
|
|
1905
1905
|
disabled={false}
|
|
1906
1906
|
type="submit"
|
|
1907
1907
|
>
|
|
@@ -1961,12 +1961,12 @@ exports[`Text-only email widget with copyColor renders correctly 1`] = `
|
|
|
1961
1961
|
|
|
1962
1962
|
.c21:disabled {
|
|
1963
1963
|
cursor: not-allowed;
|
|
1964
|
-
opacity: 0.
|
|
1964
|
+
opacity: 0.35;
|
|
1965
1965
|
}
|
|
1966
1966
|
|
|
1967
1967
|
.c22:disabled {
|
|
1968
1968
|
cursor: not-allowed;
|
|
1969
|
-
opacity: 0.
|
|
1969
|
+
opacity: 0.35;
|
|
1970
1970
|
}
|
|
1971
1971
|
|
|
1972
1972
|
.c6 {
|
|
@@ -2541,7 +2541,7 @@ exports[`Text-only email widget with copyColor renders correctly 1`] = `
|
|
|
2541
2541
|
<button
|
|
2542
2542
|
className="c21 c22"
|
|
2543
2543
|
data-test="subscribe-button"
|
|
2544
|
-
data-testid="
|
|
2544
|
+
data-testid="ButtonWithStates"
|
|
2545
2545
|
disabled={false}
|
|
2546
2546
|
type="submit"
|
|
2547
2547
|
>
|
|
@@ -41,12 +41,12 @@ exports[`renders correctly 1`] = `
|
|
|
41
41
|
|
|
42
42
|
.c15:disabled {
|
|
43
43
|
cursor: not-allowed;
|
|
44
|
-
opacity: 0.
|
|
44
|
+
opacity: 0.35;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.c16:disabled {
|
|
48
48
|
cursor: not-allowed;
|
|
49
|
-
opacity: 0.
|
|
49
|
+
opacity: 0.35;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
.c1 {
|
|
@@ -442,7 +442,7 @@ exports[`renders correctly 1`] = `
|
|
|
442
442
|
<button
|
|
443
443
|
className="c15 c16"
|
|
444
444
|
data-test="subscribe-button"
|
|
445
|
-
data-testid="
|
|
445
|
+
data-testid="ButtonWithStates"
|
|
446
446
|
disabled={true}
|
|
447
447
|
type="submit"
|
|
448
448
|
>
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useState, useEffect, useCallback, useRef
|
|
3
|
+
} from 'react';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
import PulseLoader from 'react-spinners/PulseLoader';
|
|
6
|
+
import Cross from '../../Atoms/Icons/Cross';
|
|
7
|
+
import Filter from '../../Atoms/Icons/Filter';
|
|
8
|
+
import Undo from '../../Atoms/Icons/Undo';
|
|
9
|
+
import Text from '../../Atoms/Text/Text';
|
|
10
|
+
import FilterCardNodes from './FilterCardNodes';
|
|
11
|
+
import { preprocessNodes, renderNodeLimit } from './_utils/_utils';
|
|
12
|
+
import Button from '../../Atoms/Button/Button';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
Container,
|
|
16
|
+
OuterWrapper,
|
|
17
|
+
HeaderWrapper,
|
|
18
|
+
Title,
|
|
19
|
+
Body,
|
|
20
|
+
ControlsWrapper,
|
|
21
|
+
ShowHideFiltersButton,
|
|
22
|
+
ClearSelectionButton,
|
|
23
|
+
ResultsWrapper,
|
|
24
|
+
FilterButtonsWrapper,
|
|
25
|
+
FilterButton,
|
|
26
|
+
ShowMoreButtonWrapper
|
|
27
|
+
} from './FilterCard.style';
|
|
28
|
+
|
|
29
|
+
const FilterCard = ({ data }) => {
|
|
30
|
+
const {
|
|
31
|
+
body,
|
|
32
|
+
id,
|
|
33
|
+
filterCardNodes,
|
|
34
|
+
title,
|
|
35
|
+
firstFilterCardNodeAsHero = false,
|
|
36
|
+
loadingBehaviour = null,
|
|
37
|
+
paddingAbove = '0rem',
|
|
38
|
+
paddingBelow = '1rem',
|
|
39
|
+
pageBackgroundColour = 'transparent'
|
|
40
|
+
} = data;
|
|
41
|
+
|
|
42
|
+
// Not currently a CMS option, but keeping it extrapolated for good measure for now:
|
|
43
|
+
const showMoreIncrement = 6;
|
|
44
|
+
|
|
45
|
+
// Load state and content:
|
|
46
|
+
const [processedNodes, setProcessedNodes] = useState(false);
|
|
47
|
+
const [processedTags, setProcessedTags] = useState(false);
|
|
48
|
+
const [hideShowMoreButton, setHideShowMoreButton] = useState(false);
|
|
49
|
+
const initialNodeLimit = useRef(renderNodeLimit(loadingBehaviour, firstFilterCardNodeAsHero));
|
|
50
|
+
|
|
51
|
+
// Keep track of user interactions:
|
|
52
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
53
|
+
const [currentFilters, setCurrentFilters] = useState([]);
|
|
54
|
+
const [totalNodes, setTotalNodes] = useState(0);
|
|
55
|
+
|
|
56
|
+
// Determine our display behaviour based on the CMS option
|
|
57
|
+
const [nodeRenderLimit, setNodeRenderLimit] = useState(initialNodeLimit.current);
|
|
58
|
+
|
|
59
|
+
// Transform the Contentful output for our use case on initialisation:
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let preprocessTimer;
|
|
62
|
+
|
|
63
|
+
const { outputNodes, outputTags } = preprocessNodes(filterCardNodes);
|
|
64
|
+
|
|
65
|
+
// Only go ahead once we've got the goods:
|
|
66
|
+
if (outputNodes && outputTags) {
|
|
67
|
+
// Pause for a second before going ahead, to prevent nasty Loader
|
|
68
|
+
// flashes, and let the user register that something has happened:
|
|
69
|
+
preprocessTimer = setTimeout(() => {
|
|
70
|
+
setProcessedNodes(outputNodes);
|
|
71
|
+
setProcessedTags(outputTags);
|
|
72
|
+
}, 1000);
|
|
73
|
+
}
|
|
74
|
+
return () => clearTimeout(preprocessTimer);
|
|
75
|
+
}, [filterCardNodes]);
|
|
76
|
+
|
|
77
|
+
// Adds/removes a clicked filter tag button from the state array accordingly:
|
|
78
|
+
const updateFilters = useCallback(thisTag => {
|
|
79
|
+
// When updating filters, reset render and 'Show More' displays config back to defaults:
|
|
80
|
+
setNodeRenderLimit(initialNodeLimit.current);
|
|
81
|
+
setHideShowMoreButton(true);
|
|
82
|
+
|
|
83
|
+
// Cache current state:
|
|
84
|
+
let updatedFilters = currentFilters;
|
|
85
|
+
|
|
86
|
+
if (updatedFilters.includes(thisTag)) {
|
|
87
|
+
// Create 2 slices either side of the tag we're removing, and combine into a new array
|
|
88
|
+
updatedFilters = [
|
|
89
|
+
...updatedFilters.slice(0, updatedFilters.indexOf(thisTag)),
|
|
90
|
+
...updatedFilters.slice(updatedFilters.indexOf(thisTag) + 1)
|
|
91
|
+
];
|
|
92
|
+
} else {
|
|
93
|
+
updatedFilters.push(thisTag);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Spread as a new array to trigger re-render:
|
|
97
|
+
setCurrentFilters([...updatedFilters]);
|
|
98
|
+
}, [currentFilters]);
|
|
99
|
+
|
|
100
|
+
const showHideFilterActive = !(!showFilters && currentFilters.length === 0);
|
|
101
|
+
|
|
102
|
+
const currentTotal = `${totalNodes} ${totalNodes === 1 ? 'result' : 'results'}`;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Container
|
|
106
|
+
$paddingAbove={paddingAbove}
|
|
107
|
+
$paddingBelow={paddingBelow}
|
|
108
|
+
$pageBackgroundColour={pageBackgroundColour}
|
|
109
|
+
data-testid="FilterCard"
|
|
110
|
+
id={id}
|
|
111
|
+
>
|
|
112
|
+
<OuterWrapper>
|
|
113
|
+
<HeaderWrapper>
|
|
114
|
+
|
|
115
|
+
<Title tag="h1">
|
|
116
|
+
{title}
|
|
117
|
+
</Title>
|
|
118
|
+
|
|
119
|
+
<Body>
|
|
120
|
+
{body}
|
|
121
|
+
</Body>
|
|
122
|
+
|
|
123
|
+
{/* Only render the controls once all content has been fully processed: */}
|
|
124
|
+
{(processedTags && processedNodes) ? (
|
|
125
|
+
<>
|
|
126
|
+
<ControlsWrapper>
|
|
127
|
+
<ShowHideFiltersButton
|
|
128
|
+
color="white"
|
|
129
|
+
$isActive={showHideFilterActive}
|
|
130
|
+
// As this button is acting as a checkbox (toggle-style),
|
|
131
|
+
// let assistive technologies know its role and state:
|
|
132
|
+
role="checkbox"
|
|
133
|
+
aria-checked={showFilters}
|
|
134
|
+
onClick={() => { setShowFilters(!showFilters); }}
|
|
135
|
+
icon={showFilters ? <Cross /> : <Filter />}
|
|
136
|
+
data-testid="FilterCard-ShowHideButton"
|
|
137
|
+
>
|
|
138
|
+
{showFilters ? 'Hide Filters' : 'Show Filters'}
|
|
139
|
+
</ShowHideFiltersButton>
|
|
140
|
+
|
|
141
|
+
<ClearSelectionButton
|
|
142
|
+
color="white"
|
|
143
|
+
icon={<Undo />}
|
|
144
|
+
$show={showHideFilterActive}
|
|
145
|
+
disabled={currentFilters.length === 0}
|
|
146
|
+
onClick={() => {
|
|
147
|
+
// Reset everything:
|
|
148
|
+
setCurrentFilters([]);
|
|
149
|
+
setNodeRenderLimit(initialNodeLimit.current);
|
|
150
|
+
setHideShowMoreButton(true);
|
|
151
|
+
}}
|
|
152
|
+
data-testid="FilterCard-ClearSelectionButton"
|
|
153
|
+
>
|
|
154
|
+
Clear selection
|
|
155
|
+
</ClearSelectionButton>
|
|
156
|
+
|
|
157
|
+
<ResultsWrapper
|
|
158
|
+
data-testid="FilterCard-Results"
|
|
159
|
+
role="status"
|
|
160
|
+
>
|
|
161
|
+
<Text tag="span">
|
|
162
|
+
{ currentTotal }
|
|
163
|
+
</Text>
|
|
164
|
+
</ResultsWrapper>
|
|
165
|
+
|
|
166
|
+
</ControlsWrapper>
|
|
167
|
+
|
|
168
|
+
<FilterButtonsWrapper
|
|
169
|
+
$show={showFilters}
|
|
170
|
+
data-testid="FilterCard-FilterButtonsWrapper"
|
|
171
|
+
>
|
|
172
|
+
{Object.keys(processedTags).map(key => {
|
|
173
|
+
const { tag, selectedIcon, unselectedIcon } = processedTags[key];
|
|
174
|
+
const isSelected = currentFilters.includes(tag);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<FilterButton
|
|
178
|
+
key={tag}
|
|
179
|
+
type="button"
|
|
180
|
+
color="grey_light"
|
|
181
|
+
value={tag}
|
|
182
|
+
name={tag}
|
|
183
|
+
iconLeft
|
|
184
|
+
$isSelected={isSelected}
|
|
185
|
+
// Also acting as a checkbox, so let assistive technologies know too:
|
|
186
|
+
role="checkbox"
|
|
187
|
+
aria-checked={isSelected}
|
|
188
|
+
icon={isSelected ? selectedIcon : unselectedIcon}
|
|
189
|
+
onClick={() => { updateFilters(tag); }}
|
|
190
|
+
// Force a re-render to trigger our flash-reducing, fade-in CSS animation:
|
|
191
|
+
iconKey={`${isSelected}`}
|
|
192
|
+
data-testid="FilterCard-FilterButton"
|
|
193
|
+
>
|
|
194
|
+
{tag}
|
|
195
|
+
</FilterButton>
|
|
196
|
+
);
|
|
197
|
+
})}
|
|
198
|
+
</FilterButtonsWrapper>
|
|
199
|
+
</>
|
|
200
|
+
)
|
|
201
|
+
// If content is still loading, shown the nifty loader:
|
|
202
|
+
: <PulseLoader color="black" style={{ textAlign: 'center', display: 'block' }} />
|
|
203
|
+
}
|
|
204
|
+
</HeaderWrapper>
|
|
205
|
+
|
|
206
|
+
{/* Render the content itself once it's fully processed */}
|
|
207
|
+
{(processedTags && processedNodes) && (
|
|
208
|
+
<FilterCardNodes
|
|
209
|
+
processedNodes={processedNodes}
|
|
210
|
+
firstFilterCardNodeAsHero={firstFilterCardNodeAsHero}
|
|
211
|
+
nodeRenderLimit={nodeRenderLimit}
|
|
212
|
+
currentFilters={currentFilters}
|
|
213
|
+
updateTotalResult={setTotalNodes}
|
|
214
|
+
hideShowMoreButton={setHideShowMoreButton}
|
|
215
|
+
data-testid="FilterCard-Nodes"
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{/* Only show the Show More button when appropriate,
|
|
220
|
+
hideShowMoreButton set within FilterCardNodes */}
|
|
221
|
+
{(!hideShowMoreButton && processedTags && processedNodes) && (
|
|
222
|
+
<ShowMoreButtonWrapper>
|
|
223
|
+
<Button
|
|
224
|
+
onClick={() => setNodeRenderLimit(nodeRenderLimit + showMoreIncrement)}
|
|
225
|
+
data-testid="FilterCard-ShowMoreButton"
|
|
226
|
+
>
|
|
227
|
+
Show more
|
|
228
|
+
</Button>
|
|
229
|
+
</ShowMoreButtonWrapper>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
</OuterWrapper>
|
|
233
|
+
</Container>
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
FilterCard.propTypes = {
|
|
238
|
+
// CMS content:
|
|
239
|
+
data: PropTypes.shape({
|
|
240
|
+
id: PropTypes.string.isRequired,
|
|
241
|
+
title: PropTypes.string.isRequired,
|
|
242
|
+
body: PropTypes.oneOfType([
|
|
243
|
+
PropTypes.node,
|
|
244
|
+
PropTypes.string
|
|
245
|
+
]).isRequired,
|
|
246
|
+
paddingAbove: PropTypes.string,
|
|
247
|
+
paddingBelow: PropTypes.string,
|
|
248
|
+
pageBackgroundColour: PropTypes.string,
|
|
249
|
+
loadingBehaviour: PropTypes.string,
|
|
250
|
+
firstFilterCardNodeAsHero: PropTypes.bool,
|
|
251
|
+
filterCardNodes: PropTypes.arrayOf(
|
|
252
|
+
PropTypes.shape({
|
|
253
|
+
id: PropTypes.string.isRequired,
|
|
254
|
+
title: PropTypes.string.isRequired,
|
|
255
|
+
label: PropTypes.string.isRequired,
|
|
256
|
+
heading: PropTypes.string.isRequired,
|
|
257
|
+
ctaText: PropTypes.string.isRequired,
|
|
258
|
+
url: PropTypes.string.isRequired,
|
|
259
|
+
description: PropTypes.string,
|
|
260
|
+
// GatsbyImageData deconstruction to happen within CRcom repo to feed these:
|
|
261
|
+
image: PropTypes.string.isRequired,
|
|
262
|
+
imageLow: PropTypes.string.isRequired,
|
|
263
|
+
imageSet: PropTypes.string.isRequired,
|
|
264
|
+
imageAltText: PropTypes.string.isRequired,
|
|
265
|
+
filterTags: PropTypes.arrayOf(
|
|
266
|
+
PropTypes.shape({
|
|
267
|
+
tag: PropTypes.string.isRequired,
|
|
268
|
+
title: PropTypes.string.isRequired,
|
|
269
|
+
filterIconSelected: PropTypes.shape({
|
|
270
|
+
file: PropTypes.shape({
|
|
271
|
+
url: PropTypes.string.isRequired
|
|
272
|
+
})
|
|
273
|
+
}),
|
|
274
|
+
filterIconUnselected: PropTypes.shape({
|
|
275
|
+
file: PropTypes.shape({
|
|
276
|
+
url: PropTypes.string.isRequired
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
)
|
|
281
|
+
})
|
|
282
|
+
).isRequired
|
|
283
|
+
}).isRequired
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export default FilterCard;
|