casey-chow/tigertrade

View on GitHub
client/src/components/ListingCard.js

Summary

Maintainability
F
4 days
Test Coverage
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
  Link,
  withRouter,
  propTypes as routerPropTypes,
} from 'react-router-dom';
import moment from 'moment-timezone';
import Radium, { Style } from 'radium';

import {
  Card,
  CardActions,
  CardHeader,
  CardMedia,
  CardText,
} from 'material-ui/Card';
import FlatButton from 'material-ui/FlatButton';
import Dialog from 'material-ui/Dialog';
import Chip from 'material-ui/Chip';

import { grey300 } from 'material-ui/styles/colors';

import EmailIcon from 'material-ui/svg-icons/communication/email';
import DeleteIcon from 'material-ui/svg-icons/action/delete';
import ModeEdit from 'material-ui/svg-icons/editor/mode-edit';
import FavoriteIcon from 'material-ui/svg-icons/action/favorite';
import LinkIcon from 'material-ui/svg-icons/content/link';
import MoneyIcon from 'material-ui/svg-icons/editor/monetization-on';

import Lightbox from 'react-images';

import ContactSellerForm from './ContactSellerForm';

import { mediaQueries } from '../helpers/breakpoints';
import { redirectToCas } from '../helpers/cas';
import { mailSeller } from './../actions/users';
import { loadListings, loadListing, editListing, deleteListing, starListing, updateListing } from './../actions/listings';

import './ListingCard.css';

const mapStateToProps = state => ({
  currentUser: state.currentUser,
});

@withRouter
@connect(mapStateToProps)
@Radium
export default class ListingCard extends React.Component {

  static propTypes = {
    ...routerPropTypes,
    currentUser: PropTypes.shape({
      keyId: PropTypes.number,
      loggedIn: PropTypes.bool,
    }).isRequired,
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
    }).isRequired,
    dispatch: PropTypes.func.isRequired,
    expanded: PropTypes.bool.isRequired,
    onExpandChange: PropTypes.func,
    singleton: PropTypes.bool,
    listing: PropTypes.shape({
      keyId: PropTypes.number,
      creationDate: PropTypes.string,
      lastModificationDate: PropTypes.string,
      title: PropTypes.string.isRequired,
      description: PropTypes.string,
      userId: PropTypes.number.isRequired,
      username: PropTypes.string.isRequired,
      price: PropTypes.number,
      status: PropTypes.string,
      isStarred: PropTypes.bool,
      expirationDate: PropTypes.string,
      isActive: PropTypes.bool,
      thumbnail: PropTypes.string,
      photos: PropTypes.array,
    }).isRequired,
  };

  static defaultProps = {
    currentUser: {
      keyId: -1,
      loggedIn: false,
    },
    singleton: false,
    expanded: false,
    onExpandChange: () => {},
  };

  static styles = {
    // Using a stupid style hack with classes to make this work
    // with Material UI, because Material UI is pretty shit for styling.
    cardExpanded: {
      margin: '1.5rem 0',
      mediaQueries: {
        [mediaQueries.mediumUp]: {
          margin: '1.5rem -3rem',
        },
      },
    },
    cardContentsShown: {
      maxHeight: '1000px',
      transition: 'max-height 0.5s ease-in',
      overflow: 'hidden',
    },
    cardContentsHidden: {
      maxHeight: '0',
      transition: 'max-height 0.15s ease-out',
      overflow: 'hidden',
    },
    thumbnail: {
      minWidth: undefined,
      maxHeight: '300px',
      width: 'auto',
    },
    listingImageButton: {
      padding: 0,
      backgroundColor: 'transparent',
      border: 'none',
    },
  }

  state = {
    contactOpen: false,
    lightboxImage: -1,
    lightboxOpen: false,
  }

  handleContactOpen = () => {
    if (!this.props.currentUser.loggedIn) {
      redirectToCas();
      return;
    }

    this.setState({ contactOpen: true });
  }

  handleContactClose = () => {
    this.setState({ contactOpen: false });
  }


  handleExpandChange = (expanded) => {
    this.props.onExpandChange(expanded, this.props.listing.keyId);
  }

  handleSubmit = (data) => {
    this.props.dispatch(mailSeller(
      this.props.listing,
      data,
      `Successfully contacted seller for ${this.props.listing.title}`,
    ));
    this.handleContactClose();
  }

  handleEdit = () => {
    this.props.dispatch(editListing(this.props.listing));
  }

  handleDelete = () => {
    this.props.dispatch(deleteListing(
      this.props.listing,
      `Successfully deleted listing ${this.props.listing.title}`,
    )).then(() => {
      if (this.props.singleton) {
        this.props.dispatch(loadListings({ query: { isMine: true }, reset: true }));
        this.props.history.push('/listings/mine');
      } else {
        this.props.dispatch(loadListings({}));
      }
    });
  }

  handleStar = () => {
    this.props.dispatch(starListing(
      this.props.listing,
    )).then(() => {
      if (this.props.singleton) {
        this.props.dispatch(loadListing(this.props.listing.keyId));
      } else {
        this.props.dispatch(loadListings({}));
      }
    });
  }

  handleSold = () => {
    this.props.dispatch(updateListing({
      ...this.props.listing,
      isActive: !this.props.listing.isActive,
    })).then(() => {
      if (this.props.singleton) {
        this.props.dispatch(loadListing(this.props.listing.keyId));
      } else {
        this.props.dispatch(loadListings({}));
      }
    });
  }

  formatDescription = desc => desc.split('\n').map(
    // eslint-disable-next-line react/no-array-index-key
    (line, idx) => <p key={line + idx}>{line}</p>);

  // http://stackoverflow.com/a/14428340/237904
  formatPrice = n => `$${(n / 100).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')}`;

  render() {
    const { listing, expanded } = this.props;
    const styles = ListingCard.styles;

    const favoriteButtonBackground = this.props.listing.isStarred ? grey300 : 'transparent';
    const soldButtonBackground = this.props.listing.isActive ? 'transparent' : grey300;

    const price = this.formatPrice(listing.price);
    const creationDate = listing.creationDate
      ? ` • Created ${moment.tz(listing.creationDate, 'America/New_York').format('M/D/YY')}`
      : '';
    const expirationDate = listing.expirationDate
      ? ` • Expires ${moment.tz(listing.expirationDate, 'America/New_York').format('M/D/YY')}`
      : '';
    const subtitle = `${price}${creationDate}${expirationDate}`;

    return (
      <div>
        <Style
          scopeSelector=".listing-card-expanded"
          rules={styles.cardExpanded}
        />
        <Card
          onExpandChange={this.handleExpandChange}
          expanded={expanded}
          className={expanded && 'listing-card-expanded'}
        >
          <CardHeader
            title={listing.title}
            subtitle={subtitle}
            actAsExpander
          />

          <div style={expanded ? styles.cardContentsShown : styles.cardContentsHidden}>

            { listing.photos && listing.photos.length > 0 &&

              <CardMedia>
                <div className="wrapper">
                  <div className="scrolls">
                    <div className="imageDiv">
                      {
                        listing.photos.map(
                          (image, i) =>
                            <button
                              key={image}
                              onClick={
                                event => this.setState({ lightboxImage: i, lightboxOpen: true })
                              }
                              style={styles.listingImageButton}
                            >
                              { expanded && <img alt="listing" src={image} /> }
                            </button>,
                          )
                      }
                    </div>
                  </div>
                </div>
                { this.state.lightboxOpen &&
                <Lightbox
                  images={listing.photos.map(image => ({ src: image }))}
                  onClose={() => this.setState({ lightboxOpen: false })}
                  isOpen={this.state.lightboxOpen}
                  currentImage={this.state.lightboxImage}
                  onClickNext={() => this.setState({ lightboxImage: this.state.lightboxImage + 1 })}
                  onClickPrev={() => this.setState({ lightboxImage: this.state.lightboxImage - 1 })}
                  backdropClosesModal
                /> }
              </CardMedia>
            }

            { listing.description &&
              <CardText>
                {this.formatDescription(listing.description)}
              </CardText>
            }

            <CardActions>
              { this.props.currentUser.keyId !== listing.userId ?
                <FlatButton primary icon={<EmailIcon />} label="Contact Seller" onTouchTap={this.handleContactOpen} /> :
              [
                <FlatButton primary icon={<ModeEdit />} label="Edit" onTouchTap={this.handleEdit} key={0} />,
                <FlatButton primary icon={<DeleteIcon />} label="Delete" onTouchTap={this.handleDelete} key={1} />,
                <FlatButton primary icon={<MoneyIcon />} backgroundColor={soldButtonBackground} label="Mark as Sold" onTouchTap={this.handleSold} key={2} />,
              ]
              }

              <FlatButton secondary icon={<FavoriteIcon />} backgroundColor={favoriteButtonBackground} label="Favorite" onTouchTap={this.handleStar} />
              <Link to={`/listing/${this.props.listing.keyId}`}><FlatButton icon={<LinkIcon />} label="Permalink" /></Link>
            </CardActions>

          </div>
        </Card>
        {this.state.contactOpen &&
          <Dialog
            title="Contact Seller"
            modal={false}
            open={this.state.contactOpen}
            onRequestClose={this.handleContactClose}
          >
            <div style={{ display: 'flex', flexWrap: 'wrap' }}>
              <div style={{ marginBottom: '0.5rem' }}>
                If you are interested in this item, you can let the seller know here.
                We&rsquo;ll send the seller an email about your interest so that you
                can further discuss and make plans.
              </div>
              <div style={{ margin: '10px 0.625rem 0 0' }}>To:</div>
              <Chip style={{ margin: '0.3rem 0 1rem' }}>{this.props.listing.username}@princeton.edu</Chip>
            </div>
            <ContactSellerForm
              onSubmit={this.handleSubmit}
              initialValues={{ message: `Hi! I'm interested in buying "${listing.title}".` }}
            />
          </Dialog>
        }
      </div>
    );
  }
}