Cards typography (#139)

* Fixed some ESLint errors

* Better card's design for the editorial process.

- Use Card component from react-toolbox
- Added "Edit" buttons for cards
- Cleaned up CSS and JS

Fixes #125

* Better ImageCard and card list view. Fixes #125

* Use collection label instead of name on the CollectionPage
This commit is contained in:
Andrey Okonetchnikov 2016-10-26 19:51:50 +02:00 committed by Cássio Souza
parent 434f45c97c
commit c3b4fd9013
8 changed files with 181 additions and 158 deletions

View File

@ -3,9 +3,9 @@ import ImageCard from './Cards/ImageCard';
import AlltypeCard from './Cards/AlltypeCard';
const Cards = {
_unknown: UnknownCard,
unknown: UnknownCard,
image: ImageCard,
alltype: AlltypeCard
alltype: AlltypeCard,
};
export default Cards;

View File

@ -1,18 +1,4 @@
.root {
width: 240px;
cursor: pointer;
}
.root h1 {
font-size: 17px;
}
.root h2 {
font-weight: 400;
font-size: 15px;
}
.root p {
color: #555;
font-size: 14px;
margin-top: 5px;
}

View File

@ -1,31 +1,51 @@
import React, { PropTypes } from 'react';
import { Card } from '../UI';
import { Card, CardMedia, CardTitle, CardText } from 'react-toolbox/lib/card';
import styles from './ImageCard.css';
export default class ImageCard extends React.Component {
render() {
const { onClick, onImageLoaded, image, text, description } = this.props;
return (
<Card onClick={onClick} className={styles.root}>
<img src={image} onLoad={onImageLoaded} />
<h2>{text}</h2>
{description ? <p>{description}</p> : null}
const ImageCard = (
{
author,
description,
image,
text,
onClick,
onImageLoaded,
}) => (
<Card
onClick={onClick}
className={styles.root}
>
<CardTitle
title={text}
subtitle={`by ${ author }`}
/>
{
image && <CardMedia aspectRatio="wide">
<img
src={image}
alt={text}
onLoad={onImageLoaded}
/>
</CardMedia>
}
{ description && <CardText>{ description }</CardText> }
</Card>
);
}
}
ImageCard.propTypes = {
author: PropTypes.string,
image: PropTypes.string,
onClick: PropTypes.func,
onImageLoaded: PropTypes.func,
text: PropTypes.string.isRequired,
description: PropTypes.string
description: PropTypes.string,
};
ImageCard.defaultProps = {
onClick: function() {},
onImageLoaded: function() {}
onClick: () => {
},
onImageLoaded: () => {
},
};
export default ImageCard;

View File

@ -1,13 +1,24 @@
import React, { PropTypes } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Map } from 'immutable';
import Bricks from 'bricks.js';
import { throttle } from 'lodash';
import bricks from 'bricks.js';
import Waypoint from 'react-waypoint';
import history from '../routing/history';
import Cards from './Cards';
import _ from 'lodash';
export default class EntryListing extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired,
collections: PropTypes.oneOfType([
ImmutablePropTypes.map,
ImmutablePropTypes.iterable,
]).isRequired,
entries: ImmutablePropTypes.list,
onPaginate: PropTypes.func.isRequired,
page: PropTypes.number,
};
constructor(props) {
super(props);
this.bricksInstance = null;
@ -24,13 +35,13 @@ export default class EntryListing extends React.Component {
],
};
this.updateBricks = _.throttle(this.updateBricks.bind(this), 30);
this.updateBricks = throttle(this.updateBricks.bind(this), 30);
this.handleLoadMore = this.handleLoadMore.bind(this);
}
componentDidMount() {
this.bricksInstance = Bricks({
container: this._entries,
this.bricksInstance = bricks({
container: this.containerNode,
packed: this.bricksConfig.packed,
sizes: this.bricksConfig.sizes,
});
@ -45,7 +56,8 @@ export default class EntryListing extends React.Component {
}
componentDidUpdate(prevProps) {
if ((prevProps.entries === undefined || prevProps.entries.size === 0) && this.props.entries.size === 0) {
if ((prevProps.entries === undefined || prevProps.entries.size === 0)
&& this.props.entries.size === 0) {
return;
}
@ -61,16 +73,18 @@ export default class EntryListing extends React.Component {
}
cardFor(collection, entry, link) {
const cartType = collection.getIn(['card', 'type']) || 'alltype';
const card = Cards[cartType] || Cards._unknown;
const cardType = collection.getIn(['card', 'type']) || 'alltype';
const card = Cards[cardType] || Cards.unknown;
return React.createElement(card, {
key: entry.get('slug'),
author: entry.getIn(['data', 'author']),
collection,
onClick: history.push.bind(this, link),
onImageLoaded: this.updateBricks,
text: entry.get('label') ? entry.get('label') : entry.getIn(['data', collection.getIn(['card', 'text'])]),
description: entry.getIn(['data', collection.getIn(['card', 'description'])]),
image: entry.getIn(['data', collection.getIn(['card', 'image'])]),
link,
text: entry.get('label') ? entry.get('label') : entry.getIn(['data', collection.getIn(['card', 'text'])]),
onClick: history.push.bind(this, link),
onImageLoaded: this.updateBricks,
});
}
@ -78,6 +92,10 @@ export default class EntryListing extends React.Component {
this.props.onPaginate(this.props.page + 1);
}
handleRef = (node) => {
this.containerNode = node;
};
renderCards = () => {
const { collections, entries } = this.props;
if (Map.isMap(collections)) {
@ -86,35 +104,25 @@ export default class EntryListing extends React.Component {
const path = `/collections/${ collectionName }/entries/${ entry.get('slug') }`;
return this.cardFor(collections, entry, path);
});
} else {
}
return entries.map((entry) => {
const collection = collections.filter(collection => collection.get('name') === entry.get('collection')).first();
const collection = collections
.filter(collection => collection.get('name') === entry.get('collection')).first();
const path = `/collections/${ collection.get('name') }/entries/${ entry.get('slug') }`;
return this.cardFor(collection, entry, path);
});
}
};
render() {
const { children } = this.props;
const cards = this.renderCards();
return (<div>
return (
<div>
<h1>{children}</h1>
<div ref={c => this._entries = c}>
{cards}
<div ref={this.handleRef}>
{ this.renderCards() }
<Waypoint onEnter={this.handleLoadMore} />
</div>
</div>);
</div>
);
}
}
EntryListing.propTypes = {
children: PropTypes.node.isRequired,
collections: PropTypes.oneOfType([
ImmutablePropTypes.map,
ImmutablePropTypes.iterable,
]).isRequired,
entries: ImmutablePropTypes.list,
onPaginate: PropTypes.func.isRequired,
page: PropTypes.number,
};

View File

@ -1,54 +1,34 @@
:root {
--highlightColor: #e1eeea;
--defaultFontSize: 1em;
}
.container {
display: table;
width: 100%;
display: flex;
justify-content: space-between;
}
.column {
display: table-cell;
text-align: center;
width: 33%;
height: 100%;
flex: 1 33%;
margin: -10px;
padding: 10px;
max-width: 33%;
transition: background-color .5s ease;
& h2 {
font-size: 16px;
}
}
.highlighted {
background-color: #e1eeea;
.columnHovered {
composes: column;
background-color: var(--highlightColor);
}
.column:not(:last-child) {
padding-right: 20px;
.columnHeading {
font-size: var(--defaultFontSize);
}
.draggable {
cursor: move;
}
.card {
width: 100% !important;
margin: 7px 0 0 10px;
padding: 7px 0;
}
.cardHeading {
font-size: 17px;
& small {
font-weight: normal;
}
}
.cardText {
color: #555;
font-size: 12px;
margin-top: 5px;
}
.button {
margin: 10px 10px 0 0;
float: right;
}
.clear::after {
content:"";
display:block;
clear:both;
margin-bottom: 10px;
}

View File

@ -1,13 +1,20 @@
import React, { PropTypes } from 'react';
import { DragSource, DropTarget, HTML5DragDrop } from 'react-simple-dnd';
import ImmutablePropTypes from 'react-immutable-proptypes';
import moment from 'moment';
import { Card } from './UI';
import { Link } from 'react-router';
import moment from 'moment';
import { Card, CardTitle, CardText, CardActions } from 'react-toolbox/lib/card';
import Button from 'react-toolbox/lib/button';
import { status, statusDescriptions } from '../constants/publishModes';
import styles from './UnpublishedListing.css';
class UnpublishedListing extends React.Component {
static propTypes = {
entries: ImmutablePropTypes.orderedMap,
handleChangeStatus: PropTypes.func.isRequired,
handlePublish: PropTypes.func.isRequired,
};
handleChangeStatus = (newStatus, dragProps) => {
const slug = dragProps.slug;
const collection = dragProps.collection;
@ -23,56 +30,78 @@ class UnpublishedListing extends React.Component {
};
renderColumns = (entries, column) => {
if (!entries) return;
if (!entries) return null;
if (!column) {
/* eslint-disable */
return entries.entrySeq().map(([currColumn, currEntries]) => (
<DropTarget key={currColumn} onDrop={this.handleChangeStatus.bind(this, currColumn)}>
{(isOver) => (
<div className={isOver ? `${styles.column} ${styles.highlighted}` : styles.column}>
<h2>{statusDescriptions.get(currColumn)}</h2>
<DropTarget
key={currColumn}
/* eslint-disable */
onDrop={this.handleChangeStatus.bind(this, currColumn)}
/* eslint-enable */
>
{isHovered => (
<div className={isHovered ? styles.columnHovered : styles.column}>
<h2 className={styles.columnHeading}>
{statusDescriptions.get(currColumn)}
</h2>
{this.renderColumns(currEntries, currColumn)}
</div>
)}
</DropTarget>
/* eslint-enable */
));
} else {
return (<div>
{entries.map((entry) => {
}
return (
<div>
{
entries.map((entry) => {
// Look for an "author" field. Fallback to username on backend implementation;
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
const link = `/editorialworkflow/${ entry.getIn(['metaData', 'collection']) }/${ entry.getIn(['metaData', 'status']) }/${ entry.get('slug') }`;
const link = `editorialworkflow/${ entry.getIn(['metaData', 'collection']) }/${ entry.getIn(['metaData', 'status']) }/${ entry.get('slug') }`;
const slug = entry.get('slug');
const ownStatus = entry.getIn(['metaData', 'status']);
const collection = entry.getIn(['metaData', 'collection']);
return (
/* eslint-disable */
<DragSource key={slug} slug={slug} collection={collection} ownStatus={ownStatus}>
<div className={styles.drag}>
<DragSource
key={slug}
slug={slug}
collection={collection}
ownStatus={ownStatus}
>
<div className={styles.draggable}>
<Card className={styles.card}>
<span className={styles.cardHeading}><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></span>
<p className={styles.cardText}>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
{(ownStatus === status.last()) &&
<button className={styles.button} onClick={this.requestPublish.bind(this, collection, slug, ownStatus)}>Publish now</button>
<CardTitle
title={entry.getIn(['data', 'title'])}
subtitle={`by ${ author }`}
/>
<CardText>
Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}
</CardText>
<CardActions>
<Link to={link}>
<Button>Edit</Button>
</Link>
{
ownStatus === status.last() &&
<Button
accent
/* eslint-disable */
onClick={this.requestPublish.bind(this, collection, slug, ownStatus)}
/* eslint-enable */
>
Publish now
</Button>
}
</CardActions>
</Card>
</div>
</DragSource>
/* eslint-enable */
);
})
}
)}
</div>);
}
};
static propTypes = {
entries: ImmutablePropTypes.orderedMap,
handleChangeStatus: PropTypes.func.isRequired,
handlePublish: PropTypes.func.isRequired,
</div>
);
};
render() {
@ -88,4 +117,4 @@ class UnpublishedListing extends React.Component {
}
}
export default HTML5DragDrop(UnpublishedListing);
export default HTML5DragDrop(UnpublishedListing); // eslint-disable-line

View File

@ -20,8 +20,8 @@ registry.registerWidget('list', ListControl, ListPreview);
registry.registerWidget('markdown', MarkdownControl, MarkdownPreview);
registry.registerWidget('image', ImageControl, ImagePreview);
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
registry.registerWidget('_unknown', UnknownControl, UnknownPreview);
registry.registerWidget('unknown', UnknownControl, UnknownPreview);
export function resolveWidget(name) {
return registry.getWidget(name) || registry.getWidget('_unknown');
return registry.getWidget(name) || registry.getWidget('unknown');
}

View File

@ -50,7 +50,7 @@ class DashboardPage extends React.Component {
page={page}
onPaginate={this.handleLoadMore}
>
{collection.get('name')}
{collection.get('label')}
</EntryListing>
:
<Loader active>{['Loading Entries', 'Caching Entries', 'This might take several minutes']}</Loader>