UNPKG

backpack-ui

Version:

Lonely Planet's Components

737 lines (624 loc) 17.5 kB
import React from "react"; import radium from "radium"; import truncate from "truncate"; import upperFirst from "lodash/upperFirst"; import { Link } from "react-router"; import { color, media } from "../../../settings.json"; import Button from "../button"; import Price from "../price"; import Bookmark from "../bookmark"; import Bullet from "../decoration/bullet"; import Icon from "../icon"; import Tooltip from "../tooltip"; import ListItemImage from "../listItemImage"; import { add, subtract, gutter, span, percentage } from "../../utils/grid"; import font from "../../utils/font"; import svgDataUri from "../../utils/svgDataUri"; import partnerActivityProviders from "../../../../../server/lib/open-planet/partnerActivityProviders"; const _ = { upperFirst }; const gridOffset = "1.8rem"; // the image is off the grid by this amount const containerWidth = span(8, "static"); const imageWidth = add([span(1, "static"), gutter("static"), gridOffset], "static"); const contentWidth = subtract([span(7, "static"), gutter("static"), gridOffset], "static"); const infoWidth = subtract([span(6, "static"), gutter("static"), "1.8rem"], "static"); const priceWidth = add([span(1, "static"), gutter("static")], "static"); const icons = { checkmark: `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="${color.featureCopy}"><path d="M32,4L12,32L0,20l4-4l8,8L28,0L32,4z"></path></svg>`, }; const styles = { container: { base: { position: "relative", }, }, image: { base: { float: "left", marginRight: percentage("15px", "335px"), overflow: "hidden", position: "relative", width: percentage("78px", "335px"), [`@media (min-width: ${media.min["768"]})`]: { marginRight: gutter("fluid", 8), width: percentage(imageWidth, containerWidth), }, }, img: { display: "block", width: "100%", }, }, content: { base: { float: "left", position: "relative", width: percentage("242px", "335px"), [`@media (min-width: ${media.min["768"]})`]: { width: percentage(contentWidth, containerWidth), }, [`@media (min-width: ${media.min["1024"]})`]: { display: "flex", }, }, }, info: { base: { [`@media (min-width: ${media.min["1024"]})`]: { float: "left", paddingRight: percentage(span(1, "static"), contentWidth), width: percentage(infoWidth, contentWidth), }, }, noFlexbox: { [`@media (min-width: ${media.min["1024"]}) and (max-width: ${1365 * 0.0625}em)`]: { float: "none", paddingRight: 0, width: "100%", }, }, }, header: { base: { overflow: "hidden", }, }, category: { base: { color: color.detailHeaderSmall, fontSize: "1rem", lineHeight: 1, marginBottom: ".5rem", textTransform: "uppercase", [`@media (max-width: ${media.max["768"]})`]: { letterSpacing: ".4px", marginTop: ".1rem", }, [`@media (min-width: ${media.min["768"]})`]: { fontSize: "1.2rem", marginBottom: "1.3rem", }, }, sponsored: { color: color.orange, }, topChoice: { color: color.red, fontWeight: 600, }, location: { [`@media (max-width: ${media.max["768"]})`]: { display: "none", }, }, }, title: { base: { color: color.darkGray, float: "left", fontSize: "2rem", fontWeight: 600, letterSpacing: "-1px", lineHeight: (24 / 20), margin: 0, maxWidth: "90%", [`@media (min-width: ${media.min["768"]})`]: { fontSize: "2.8rem", lineHeight: (34 / 28), }, }, }, bookmark: { base: { [`@media (max-width: ${media.max["768"]})`]: { bottom: "-2rem", position: "absolute", right: "-.4rem", }, [`@media (min-width: ${media.min["768"]})`]: { display: "inline-block", marginLeft: ".5rem", marginTop: ".3rem", }, }, }, largeText: { base: { color: color.featureCopy, fontSize: "12px", lineHeight: (15.6 / 12), marginTop: `${9 / 12}em`, [`@media (min-width: ${media.min["768"]})`]: { fontSize: "18px", marginTop: `${9 / 18}em`, }, }, item: { display: "inline", }, }, reviewedText: { base: { [`@media (max-width: ${media.max["768"]})`]: { display: "none", }, [`@media (min-width: ${media.min["768"]})`]: { backgroundImage: `url("${svgDataUri(icons.checkmark)}")`, backgroundPosition: "0 2px", backgroundRepeat: "no-repeat", backgroundSize: "8px 8px", color: color.featureCopy, fontFamily: font("miller"), fontSize: "1.2rem", fontStyle: "italic", lineHeight: 1, listStyle: "none", marginTop: "2.2rem", paddingLeft: "1.4rem", }, }, }, description: { base: { color: color.titleGray, fontSize: "1.4rem", lineHeight: (24 / 14), marginBottom: "2.7rem", marginTop: ".9rem", [`@media (min-width: ${media.min["768"]})`]: { fontSize: "1.8rem", lineHeight: (32 / 18), marginTop: "1.4rem", }, }, clamp: { display: "-webkit-box", overflow: "hidden", textOverflow: "ellipsis", WebkitBoxOrient: "vertical", WebkitLineClamp: 2, }, }, price: { base: { marginTop: "1.1rem", [`@media (max-width: ${media.max["768"]})`]: { display: "inline-block", }, [`@media (min-width: ${media.min["768"]})`]: { marginTop: "2.5rem", }, [`@media (min-width: ${media.min["1024"]})`]: { float: "left", textAlign: "right", width: priceWidth, }, }, noFlexbox: { [`@media (min-width: ${media.min["1024"]}) and (max-width: ${1365 * 0.0625}em)`]: { float: "none", textAlign: "left", width: "100%", }, [`@media (min-width: ${1366 * 0.0625}em)`]: { textAlign: "right", width: percentage(priceWidth, contentWidth), }, }, callToBook: { fontSize: "1.6rem", fontWeight: 600, lineHeight: 1, }, }, }; function ListItemBookable({ title, slug, type, subtype, place, price, features, image, description, sponsored, topChoice, reviewed, bookmark, bookmarkSize, id, duration, durationInfo, showTourDestinations, tourStart, tourEnd, tourMap, activityType, mobile, hidePrice, onShowPrices, }) { let descriptionString; if (typeof description === "object") { descriptionString = () => { if (description.short_description) { return description.short_description; } return description.long_description; }; } if (typeof description === "string") { descriptionString = truncate(description.replace(/(<([^>]+)>)/ig, ""), 115); } let durationString; if (typeof duration === "object" && durationInfo) { durationString = () => { if (duration.duration && duration.unit) { const unit = duration.duration === 1 ? duration.unit.replace(/s$/, "") : duration.unit; return `${duration.duration} ${unit}`; } return durationInfo; }; } if (typeof description === "string") { descriptionString = truncate(description.replace(/(<([^>]+)>)/ig, ""), 115); } let hasFlexbox = true; if (typeof document !== "undefined" && document.documentElement) { hasFlexbox = document && document.documentElement.classList ? document.documentElement.classList.contains("flexbox") : new RegExp("(^| )flexbox( |$)", "gi").test(document.documentElement.className); } return ( <div className="ListItem clearfix" id={id} style={styles.container.base}> <div className="ListItem-image" style={styles.image.base}> <Link to={slug} style={styles.image.img}> <ListItemImage title={title} image={image} /> </Link> </div> <div className="ListItem-content" style={styles.content.base} > <div className="ListItem-info" style={[ styles.info.base, !hasFlexbox && styles.info.noFlexbox, ]} > <header className="ListItem-header" style={styles.header.base}> <div className="ListItem-category" style={styles.category.base}> {sponsored && <span style={styles.category.sponsored}> Sponsored </span> } {topChoice && <span style={styles.category.topChoice}> Top Choice </span> } {subtype} {place.name && <span style={styles.category.location}> in {place.name}</span> } </div> <h2 className="ListItem-title" style={styles.title.base}> <Link to={slug} style={{ color: "inherit" }}> {title} </Link> </h2> {bookmark && <div className="ListItem-bookmark" style={styles.bookmark.base}> <Bookmark onClick={null} size={bookmarkSize} /> </div> } </header> {description && !type === "sleeping" && <div className="ListItem-description" style={styles.description.base}> <div style={styles.description.clamp} dangerouslySetInnerHTML={{ __html: description }} /> </div> } {features.length > 0 && <ul className="ListItem-features" style={styles.largeText.base}> {features.map((item, i) => ( <li style={styles.largeText.item} key={i}> {_.upperFirst(item)} {i !== features.length - 1 && <Bullet space="both" color={color.detailHeaderSmall} /> } </li> ))} </ul> } {activityType === "tours" && showTourDestinations && <div className="ListItem-tourDestinations" style={styles.largeText.base}> {(tourStart && tourEnd) && <Link to={slug} style={{ color: "inherit" }}> {!mobile && <Icon.Pin height="11px" width="11px" style={{ marginRight: `${7 / 18}em`, marginTop: `${3 / 18}em`, verticalAlign: "top", }} /> } {!mobile && tourMap && <Tooltip label={`${tourStart} to ${tourEnd}`} flyout={{ arrow: "down", size: "medium", removePadding: true, shadow: "large", style: { bottom: "40px", left: 0, position: "absolute", }, }} > <img src={tourMap} alt="" width={290} style={{ display: "block" }} /> </Tooltip> } {(mobile || !tourMap) && <span>{tourStart} to {tourEnd}</span> } </Link> } </div> } {!mobile && activityType === "activities" && <div className="ListItem-description" style={styles.description.base}> <div style={styles.description.clamp}> {descriptionString()} </div> </div> } {reviewed && <div style={styles.reviewedText.base}> Lonely Planet reviewed </div> } </div> {!hidePrice && price.amount && <div className="ListItem-price" style={[styles.price.base, !hasFlexbox && styles.price.noFlexbox]} > <Price amount={price.amount} rate={type === "partner_activity" ? durationString() : price.rate} parent="listItem" emphasized /> </div> } {hidePrice && <div className="ListItem-price" style={[styles.price.base, !hasFlexbox && styles.price.noFlexbox]} > <Button color="white" size={mobile ? "tiny" : "small"} onClick={onShowPrices} rounded border > Show price </Button> </div> } </div> </div> ); } ListItemBookable.propTypes = { /** * The name of the POI * key: name */ title: React.PropTypes.string.isRequired, /** * The URL slug of the POI; constructed via parent (pois/{id}) */ slug: React.PropTypes.string.isRequired, /** * The type of POI * key: poi_type */ type: React.PropTypes.string.isRequired, /** * The subtype of POI; i.e. Museum * key: subtypes[0] * partner-activities key: canonical_category */ subtype: React.PropTypes.string.isRequired, /** * The place data for the POI; required keys are name and type */ place: React.PropTypes.shape({ name: React.PropTypes.string, type: React.PropTypes.string, }).isRequired, /** * Price object for the POI; requires amount and rate keys * key: price_string * partner-activities key: minimum_price.formatted_amount */ price: React.PropTypes.shape({ amount: React.PropTypes.number, rate: React.PropTypes.string, }), /** * A short list of features; limited to three */ features: React.PropTypes.arrayOf(React.PropTypes.string), /** * Image src for the POI; required keys are path and orientation * partner-activities key: links.image */ image: React.PropTypes.shape({ path: React.PropTypes.string, orientation: React.PropTypes.oneOf([ "", "portrait", "landscape", ]), }), /** * Description for POI * key: review.essential, review.extension * partner-activities key: descriptions.short_description, descriptions.long_description */ description: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.object, ]), /** * Add a "sponsored" label */ sponsored: React.PropTypes.bool, /** * Add a "top choice" label */ topChoice: React.PropTypes.bool, /** * If the POI has been reviewed by Lonely Planet staff */ reviewed: React.PropTypes.bool, /** * If list item is able to be bookmarked */ bookmark: React.PropTypes.bool, /** * Size of bookmark component */ bookmarkSize: React.PropTypes.oneOf([ "small", "large", ]), /** * Unique ID for item * key: id */ id: React.PropTypes.string, /** * The length of the tour or activity; partner activity only * partner-activities key: duration_string */ duration: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.object, ]), /** * If tour, whether or not to show tour destination info */ showTourDestinations: React.PropTypes.bool, /** * City name where the tour or activity begins; partner activity only * partner-activities key: departure_info */ tourStart: React.PropTypes.string, /** * City name where the tour or activity begins; partner activity only * partner-activities key: return_info */ tourEnd: React.PropTypes.string, /** * Image URL of the tour or activity; partner activity only * partner-activities key: links.map */ tourMap: React.PropTypes.string, /** * String to determine whether to show activity or tour info */ activityType: React.PropTypes.string, /** * Default string for duration */ durationInfo: React.PropTypes.string, /** * Is the mobile layout active? */ mobile: React.PropTypes.bool, /** * Hide price and display "Show price" button instead; used with availability */ hidePrice: React.PropTypes.bool, /** * Method to run to show price; used in tandem with `hidePrice` */ onShowPrices: React.PropTypes.func, }; ListItemBookable.defaultProps = { title: "", slug: "", type: "", subtype: "", place: {}, price: {}, features: [], image: {}, description: "", sponsored: false, topChoice: false, reviewed: false, bookmark: false, bookmarkSize: "small", id: "", duration: "", difficulty: "", groupSize: {}, showTourDestinations: false, tourStart: "", tourEnd: "", tourMap: "", bookingUrl: "", activityType: "", durationInfo: "", mobile: false, hidePrice: false, onShowPrices: null, }; ListItemBookable.styles = styles; export default radium(ListItemBookable);