/
ProductCard.js
148 lines (137 loc) · 4.08 KB
/
ProductCard.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// @flow
import * as React from 'react';
import sx from '@adeira/sx';
import { Blurhash } from 'react-blurhash';
import { fbt } from 'fbt';
import { useState } from 'react';
import { warning } from '@adeira/js';
import Heading from '../Heading/Heading';
import Money from '../Money/Money';
import type { SupportedCurrencies } from '../constants';
type Props = {
+title: Fbt,
+priceUnitAmount: number,
+priceUnitAmountCurrency: SupportedCurrencies,
+imgBlurhash?: string,
+imgSrc?: string,
+imgAlt?: Fbt,
};
/**
* This component display product card with product title and product price. The recommended usage
* is as follows:
*
* 1. display grid of `Skeleton` components when loading the data
* 2. display the same grid of `ProductCards` with `imgBlurhash` (https://blurha.sh/) and `imgSrc` set
*
* This will result in a nice loading experience where the user sees:
*
* 1. grid of grey squares, after that:
* 2. grid of blurhashes instead of boring grey squares, after that:
* 3. the actual images
*
* Simple CSS grid example:
*
* ```js
* const styles = sx.create({
* productsGrid: {
* display: 'grid',
* gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
* gap: '1rem',
* },
* });
* ```
*/
export default function ProductCard(props: Props): React.Node {
const [isHovered, setIsHovered] = useState(false);
const [isImageLoaded, setImageLoaded] = useState(false);
if (props.imgSrc != null) {
warning(
props.imgAlt != null,
// $FlowExpectedError[incompatible-call]: warning expects string, not FBT
fbt(
'You should specify alternative image text via `imgAlt` property. This is an important ' +
'part of accessibility for screen reader users in order for them to understand the ' +
"content's purpose on the page.",
'accessibility warning when img alt is missing (product card component)',
),
);
}
const DEFAULT_HEIGHT = 250;
return (
<div
style={{ height: DEFAULT_HEIGHT }}
className={styles('wrapper')}
onMouseOver={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className={styles('highlightWrapper')}>
<Heading xstyle={styles.heading}>
<span className={styles(isHovered ? 'highlightHover' : 'highlight', 'highlightBase')}>
{props.title}
</span>
</Heading>
<span className={styles(isHovered ? 'highlightHover' : 'highlight', 'highlightBase')}>
<Money
priceUnitAmount={props.priceUnitAmount}
priceUnitAmountCurrency={props.priceUnitAmountCurrency}
/>
</span>
</div>
{isImageLoaded === false && props.imgBlurhash != null ? (
<Blurhash hash={props.imgBlurhash} width="100%" height={DEFAULT_HEIGHT} />
) : null}
{props.imgSrc != null ? (
<img
src={props.imgSrc}
alt={props.imgAlt}
height={DEFAULT_HEIGHT}
className={styles(isImageLoaded === false && 'imgSrcLoading', 'imgSrc')}
onLoad={() => setImageLoaded(true)}
// onError does nothing (we keep the blurhash)
/>
) : null}
</div>
);
}
const styles = sx.create({
wrapper: {
display: 'flex',
flexDirection: 'column',
backgroundColor: 'lightgrey',
position: 'relative',
},
imgSrcLoading: {
display: 'none',
},
imgSrc: {
objectFit: 'cover',
objectPosition: 'center center',
},
highlightWrapper: {
position: 'absolute',
left: 0,
top: 0,
zIndex: 2,
},
highlightBase: {
transitionProperty: 'all',
transitionDuration: '250ms',
transitionTimingFunction: 'ease-in-out',
display: 'inline-block',
marginBottom: 1,
padding: '1rem',
},
highlight: {
'color': 'rgba(var(--sx-foreground))',
'backgroundColor': 'rgba(var(--sx-background))',
'--sx-money-text-color': 'var(--sx-foreground)',
},
highlightHover: {
'color': 'rgba(var(--sx-background))',
'backgroundColor': 'rgba(var(--sx-foreground))',
'--sx-money-text-color': 'var(--sx-background)',
},
heading: {
margin: 0,
},
});