@tractorzoom/equipment-card
Version:
287 lines (260 loc) • 11.4 kB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
import {
formatNumberWithThousandSeparator,
getLocation,
getTopAttributesForCategoryAndSubCategory,
} from '@tractorzoom/equipment-attributes';
import AddRoundedIcon from '@material-ui/icons/AddRounded';
import { Box } from '@material-ui/core';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { DateTime } from 'luxon';
import Divider from '@material-ui/core/Divider';
import EquipmentCardSkeleton from './equipment-card-skeleton';
import IconButton from '@material-ui/core/IconButton';
import Lozenge from '@tractorzoom/lozenge';
import PropTypes from 'prop-types';
import Typography from '@material-ui/core/Typography';
import clsx from 'clsx';
import { useInView } from 'react-intersection-observer';
import useStyles from './styles';
const getAttrText = (attr, props) => {
const attrName = attr.name;
if (attrName === 'width' && (props['widthFeet'] || props['widthInches'])) {
const ft = props['widthFeet'];
const inches = props['widthInches'];
if (ft && inches) {
return `${formatNumberWithThousandSeparator(ft)}' ${inches}"`;
}
if (ft) {
return `${formatNumberWithThousandSeparator(ft)} ft`;
}
return `${formatNumberWithThousandSeparator(inches)} in`;
}
if (attrName === 'capacity' && (props['ton'] || props['gallon'])) {
const ton = props['ton'];
const gallon = props['gallon'];
if (ton) {
return `${formatNumberWithThousandSeparator(ton)} T`;
}
return `${formatNumberWithThousandSeparator(gallon)} Gal`;
}
if (props[attrName]) {
return formatNumberWithThousandSeparator(props[attr.name]) + ' ' + attr?.shortName;
}
return '';
};
const EquipmentCard = (props) => {
const canvasRef = useRef(null);
const classes = useStyles();
const getImageUrl = (url) => {
if (url) {
if (url.indexOf('http') >= 0) {
return url.replace('tz-prod.s3.amazonaws.com', 's3.tractorzoom.com');
}
return `https://s3.tractorzoom.com/${url}`;
}
return '';
};
const defaultImageUrl = props.imageUrl
? getImageUrl(props.imageUrl)
: props.makeImageUrl
? getImageUrl(props.makeImageUrl)
: '/img/nopicture.png';
const [imageUrl, setImageUrl] = useState(defaultImageUrl);
const isSold = props.price > 0;
const isDealer = props.sellerType === 'dealer';
const saleDate = props.saleDate ? props.saleDate : props.auctionDate;
const formattedDate = DateTime.fromISO(saleDate).toLocaleString();
const auctionDate = DateTime.fromISO(saleDate);
const formattedAuctionDate = auctionDate.toFormat('MMM d');
const formattedPrice = `$${formatNumberWithThousandSeparator(`${props.price}`)}`;
const formattedListPrice = `$${formatNumberWithThousandSeparator(`${props.listPrice}`)}`;
const isSelected = props.selectedEquipmentSet && props.selectedEquipmentSet.has(props.id);
const { ref, inView } = useInView({
threshold: 0.7,
});
const cardTitleText = `${props.year ? `${props.year} ` : ''}${props.make} ${props.model}`;
const [hasBeenSeen, setHasBeenSeen] = useState(false);
let styles = props.style;
useEffect(() => {
if (props.onVisible && inView && !hasBeenSeen) {
setHasBeenSeen(true);
props.onVisible(inView);
}
}, [props.onVisible, inView, hasBeenSeen]);
if (props.isLoading) {
return <EquipmentCardSkeleton {...props} />;
}
const attr = getTopAttributesForCategoryAndSubCategory(props.category, props.subcategory);
if (!props.handleOpen) {
styles = { ...styles, pointerEvents: 'none' };
}
const handleToggleSelected = (event) => {
props.handleEquipmentSelected();
event.stopPropagation();
};
const attributeArray = attr.map((attribute) => getAttrText(attribute, props)).filter((attr) => !!attr);
const imageError = (e) => {
e.target.onerror = null;
if (imageUrl === getImageUrl(props.makeImageUrl) || !props.makeImageUrl) {
setImageUrl('https://s3.tractorzoom.com/img/nopicture.png');
} else {
setImageUrl(getImageUrl(props.makeImageUrl));
}
};
const imageAltText = `${cardTitleText} Equipment Image`;
const NextImage = () => (
<props.imageComponent
alt={imageAltText}
height={233}
objectFit='cover'
onError={imageError}
src={imageUrl}
width={350}
/>
);
return (
<div className={classes.cardHolder}>
<canvas className={classes.canvas} ref={canvasRef}></canvas>
<Card
className={clsx(classes.root, {
[classes.selectedCard]: isSelected,
})}
data-cy='equipment-card'
data-guid={props.id}
ref={ref}
style={styles}
variant='outlined'
>
<CardActionArea
data-tour={props.shouldHaveDataTour ? 'equipment-card-action-area' : ''}
onClick={props.handleOpen}
>
<CardMedia
alt={imageAltText}
className={classes.media}
component={props.imageComponent ? NextImage : 'img'}
data-cy='equipment-card-image'
onError={imageError}
src={imageUrl}
title='Equipment Image'
/>
<CardContent classes={{ root: classes.cardContent }}>
<Box
alignItems='center'
display='flex'
justifyContent='space-between'
style={{ marginBottom: 4 }}
>
<Typography
className={classes.makeModelTitle}
component='span'
data-cy='equipment-card-make-model'
variant='h5'
>
{cardTitleText}
</Typography>
{props.sellerType && (
<Lozenge
backgroundColor={isDealer ? '#DAE7FE' : '#DFFCED'}
color={isDealer ? '#183A99' : '#045C3E'}
style={{ margin: 4 }}
>
{isDealer ? 'DEALER' : 'AUCTION'}
</Lozenge>
)}
</Box>
<div className={classes.details}>
<div data-cy='equipment-card-variable-detail'>
<Typography className={classes.primaryDetail}>
{attributeArray.length > 0 ? attributeArray[0] : '---'}
</Typography>
</div>
<Typography
className={isDealer ? classes.listPrice : classes.price}
data-cy='equipment-card-price'
>
{isSold ? formattedPrice : isDealer ? formattedListPrice : formattedAuctionDate}
</Typography>
</div>
<Typography className={classes.secondaryDetail} style={{ marginBottom: 10 }}>
{attributeArray.length > 1 ? attributeArray[1] : '\u00a0'}
</Typography>
<Divider className={classes.divider} />
<div className={classes.locationAndSaleDate}>
{isSold ? (
<>
<Typography className={classes.auctionDetailsText}>
{getLocation(props, true)}
</Typography>
<Typography className={classes.auctionDetailsText} style={{ color: '#0E1C3699' }}>
Sold {formattedDate}
</Typography>
</>
) : (
<Box display='flex' flexDirection='column'>
<Typography className={classes.auctionDetailsText}>{props.auctioneer}</Typography>
<Typography className={classes.auctionDetailsText} style={{ color: '#0E1C3699' }}>
{getLocation(props, true)}
</Typography>
</Box>
)}
</div>
</CardContent>
</CardActionArea>
{!!props.handleEquipmentSelected && (
<IconButton
aria-label='select equipment'
className={isSelected ? classes.checkedButton : classes.selectButton}
color='primary'
data-cy='equipment-card-toggle-selection-button'
data-tour={props.shouldHaveDataTour ? 'equipment-card-select-equipment' : ''}
onClick={handleToggleSelected}
title={isSelected ? 'Remove from custom average' : 'Add to custom average'}
>
{isSelected ? <CheckRoundedIcon fontSize='large' /> : <AddRoundedIcon fontSize='large' />}
</IconButton>
)}
</Card>
</div>
);
};
EquipmentCard.defaultProps = {
auctionDate: '',
distance: null,
imageUrl: '',
saleDate: '',
shouldHaveDataTour: false,
year: null,
};
EquipmentCard.propTypes = {
auctionDate: PropTypes.string,
auctioneer: PropTypes.string,
category: PropTypes.string,
distance: PropTypes.number,
handleEquipmentSelected: PropTypes.func,
handleOpen: PropTypes.func,
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
imageComponent: PropTypes.func,
imageUrl: PropTypes.string,
isLoading: PropTypes.bool,
listPrice: PropTypes.string,
make: PropTypes.string,
makeImageUrl: PropTypes.string,
model: PropTypes.string,
onVisible: PropTypes.func,
price: PropTypes.number,
saleDate: PropTypes.string,
selectedEquipmentSet: PropTypes.object,
sellerType: PropTypes.number,
shouldHaveDataTour: PropTypes.bool,
state: PropTypes.string,
style: PropTypes.object,
subcategory: PropTypes.string,
year: PropTypes.number,
};
export default EquipmentCard;