Added <Avatar> component as required by the card title component

old
Nathan Marks 2015-11-17 16:24:22 -05:00
parent 37199c22e6
commit dba68e72a5
17 changed files with 536 additions and 219 deletions

View File

@ -227,7 +227,7 @@
"react/no-danger": 0,
"react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 1,
"react/no-multi-comp": 0,
"react/no-multi-comp": 1,
"react/no-unknown-property": 1,
"react/prop-types": [2, {"ignore": ["onMouseDown", "onTouchStart"]}],
"react/react-in-jsx-scope": 1,

View File

@ -0,0 +1,67 @@
import React, { PropTypes, Component } from 'react';
import ClassNames from '../decorators/ClassNames';
import FontIcon from '../font_icon'; // ewww! :P @TODO
import style from './style';
@ClassNames(style)
class Avatar extends Component {
static propTypes = {
accent: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
className: PropTypes.string,
classNames: PropTypes.func,
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element
]),
image: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element
]),
primary: PropTypes.bool,
size: PropTypes.number.required
}
static defaultProps = {
size: 40
}
render () {
const {
accent,
children,
className,
classNames,
icon,
primary,
image,
...otherProps
} = this.props;
let component;
const classes = classNames('avatar', {
accent,
primary
}, className);
if (typeof image === 'string') {
component = <img className={style.avatarImg} src={image} />;
} else if (typeof image === 'string') {
component = <img className={style.avatarImg} src={image} />;
} else if (typeof icon === 'string') {
component = <FontIcon value="icon" />;
}
return (
<div className={classes}>{component ? component : children}</div>
);
}
}
export default Avatar;

View File

@ -0,0 +1 @@
export Avatar from './Avatar.jsx';

View File

@ -0,0 +1,11 @@
.avatar {
width: 40px;
height: 40px;
overflow: hidden;
border-radius: 50%;
}
.avatarImg {
max-width: 100%;
height: auto;
}

View File

@ -1,87 +1,36 @@
import React from 'react';
import Navigation from '../navigation';
import Ripple from '../ripple';
import style from './style';
import React, { PropTypes, Component } from 'react';
import ClassNames from '../decorators/ClassNames';
import styles from './style';
@ClassNames(styles)
class Card extends Component {
class Card extends React.Component {
static propTypes = {
actions: React.PropTypes.array,
className: React.PropTypes.string,
color: React.PropTypes.string,
image: React.PropTypes.string,
loading: React.PropTypes.bool,
onClick: React.PropTypes.func,
subtitle: React.PropTypes.string,
text: React.PropTypes.string,
title: React.PropTypes.string,
type: React.PropTypes.oneOf(['wide', 'event', 'image'])
};
static defaultProps = {
className: '',
loading: false
};
handleMouseDown = (event) => {
if (this.props.onClick) {
event.preventDefault();
this.refs.ripple.start(event);
this.props.onClick(event, this);
}
};
renderActions () {
if (this.props.actions) {
return (
<Navigation data-role='actions' className={style.navigation} actions={this.props.actions} />
);
}
}
renderTitle () {
const styleFigure = {};
const styleOverflow = {};
if (this.props.image) styleFigure.backgroundImage = `url(${this.props.image})`;
if (this.props.color) {
styleFigure.backgroundColor = this.props.color;
styleOverflow.backgroundColor = this.props.color;
}
if (this.props.title || this.props.image) {
return (
<figure className={style.figure} style={styleFigure}>
{this.props.title ? <h5 data-role='title'>{this.props.title}</h5> : null}
{this.props.subtitle ? <small data-role='subtitle'>{this.props.subtitle}</small> : null}
{this.props.color ? <div className={style.overflow} style={styleOverflow}></div> : null}
</figure>
);
}
children: PropTypes.any,
className: PropTypes.string,
classNames: PropTypes.func,
raised: PropTypes.bool
}
render () {
let className = style.root;
if (this.props.type) className += ` ${style[this.props.type]}`;
if (this.props.onClick) className += ` ${style.touch}`;
if (this.props.image || this.props.color) className += ` ${style.contrast}`;
if (this.props.color) className += ` ${style.color}`;
if (this.props.loading) className += ` ${style.loading}`;
if (this.props.className) className += ` ${this.props.className}`;
const {
children,
className,
classNames,
raised,
...otherProps
} = this.props;
const classes = classNames('card', {
'raised': raised
}, className);
return (
<div
data-react-toolbox='card'
className={className}
onMouseDown={this.handleMouseDown}
data-react-toolbox="card"
className={classes}
>
{this.renderTitle()}
{this.props.text ? <p data-role='text' className={style.text}>{this.props.text}</p> : null}
{this.renderActions()}
<Ripple
ref='ripple'
className={style.ripple}
loading={this.props.loading}
spread={2.5}
/>
{children}
</div>
);
}

View File

@ -0,0 +1,32 @@
import React, { PropTypes, Component } from 'react';
import ClassNames from '../decorators/ClassNames';
import style from './style';
@ClassNames(style)
class CardActions extends Component {
static propTypes = {
children: PropTypes.any,
className: PropTypes.string,
classNames: PropTypes.func
}
render () {
const {
children,
className,
classNames,
...otherProps
} = this.props;
const classes = classNames('cardActions', className);
return (
<div className={classes} {...otherProps}>
{children}
</div>
);
}
}
export default CardActions;

View File

@ -0,0 +1,48 @@
import React, { PropTypes, Component } from 'react';
import ClassNames from '../decorators/ClassNames';
import style from './style';
@ClassNames(style)
class CardMedia extends Component {
static propTypes = {
aspectRatio: PropTypes.oneOf([ 'wide', 'square' ]),
children: PropTypes.node,
className: PropTypes.string,
classNames: PropTypes.func,
color: PropTypes.string,
image: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
])
}
render () {
const {
aspectRatio,
children,
className,
classNames,
color,
image,
...otherProps
} = this.props;
const classes = classNames('cardMedia', aspectRatio, className);
const bgStyle = {
backgroundColor: color ? color : undefined,
backgroundImage: typeof image === 'string' ? `url('${image}')` : undefined
};
return (
<div style={bgStyle} className={classes} {...otherProps}>
<div className={style.content}>
{children}
</div>
</div>
);
}
}
export default CardMedia;

View File

@ -0,0 +1,32 @@
import React, { PropTypes, Component } from 'react';
import ClassNames from '../decorators/ClassNames';
import style from './style';
@ClassNames(style)
class CardText extends Component {
static propTypes = {
children: PropTypes.any,
className: PropTypes.string,
classNames: PropTypes.func
}
render () {
const {
children,
className,
classNames,
...otherProps
} = this.props;
const classes = classNames('cardText', className);
return (
<div className={classes} {...otherProps}>
{typeof children === 'string' ? <p>{children}</p> : children}
</div>
);
}
}
export default CardText;

View File

@ -0,0 +1,61 @@
import React, { PropTypes, Component } from 'react';
import ClassNames from '../decorators/ClassNames';
import { Avatar } from '../avatar';
import style from './style';
@ClassNames(style)
class CardTitle extends Component {
static propTypes = {
avatar: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element
]),
children: PropTypes.string,
className: PropTypes.string,
classNames: PropTypes.func,
subtitle: PropTypes.string,
title: PropTypes.string
}
render () {
let avatarComponent;
const {
avatar,
children,
className,
classNames,
subtitle,
title,
...otherProps
} = this.props;
const classes = classNames('cardTitle', {
'small': avatar,
'large': !avatar
}, className);
if (typeof avatar === 'string') {
avatarComponent = <Avatar image={avatar} />;
} else {
avatarComponent = avatar;
}
return (
<div className={classes} {...otherProps}>
{avatarComponent && (
<div className={style.avatar}>
{avatarComponent}
</div>
)}
<div>
{(title || children) && <h5 className={style.title}>{title ? title : children}</h5>}
{subtitle && <p className={style.subtitle}>{subtitle}</p>}
</div>
</div>
);
}
}
export default CardTitle;

View File

@ -1,7 +1,10 @@
$card-color-white: $color-white !default;
$card-title-height: 17.6 * $unit;
$card-width-normal: 32 * $unit;
$card-width-large: 51.2 * $unit;
$card-offset: 1.6 * $unit;
$card-navigation-offset: $card-offset / 2;
$card-text-overlay: rgba($color-black, 0.2);
$card-color-white: unquote("rgb(#{$color-white})") !default;
$card-text-overlay: unquote("rgba(#{$color-black}, 0.2)");
$card-background-color: $card-color-white;
$card-padding-sm: 8px;
$card-padding: 16px;
$card-padding-lg: 20px;
$card-font-size: $font-size-small;

View File

@ -0,0 +1,5 @@
export Card from './Card.jsx';
export CardActions from './CardActions.jsx';
export CardMedia from './CardMedia.jsx';
export CardText from './CardText.jsx';
export CardTitle from './CardTitle.jsx';

View File

@ -1,112 +1,122 @@
@import "../base";
@import "./config";
.figure {
position: relative;
.card {
// Box Model
display: flex;
min-height: $card-title-height;
flex-direction: column;
justify-content: flex-end;
background-position: center center;
background-size: cover;
> *:not(.overflow) {
z-index: $z-index-normal;
font-weight: $font-weight-normal;
border-radius: 2px;
overflow: hidden;
// Fonts
font-size: $card-font-size;
// Colors
background: $card-background-color;
// Elevation
@include shadow-2dp();
// Raised Elevation as per spec
&.raised {
@include shadow-8dp();
}
> .overflow {
}
.cardMedia {
position: relative;
background-repeat: no-repeat;
background-size: cover;
background-position: center center;
height: 0;
&.wide {
width: 100%;
padding-top: 56.25%;
}
&.square {
width: 100%;
padding-top: 100%;
}
.content {
// Positioning
position: absolute;
top: 0;
left: 0;
z-index: 0;
// Box Model
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
opacity: .75;
}
}
.text {
font-size: $font-size-small;
line-height: $font-size-big;
color: $color-text-secondary;
}
.navigation {
padding: $card-navigation-offset;
> * {
min-width: 0;
padding-right: $card-navigation-offset;
padding-left: $card-navigation-offset;
}
}
.ripple {
background-color: $color-text-secondary;
}
.root {
@include shadow-2dp();
position: relative;
.cardTitle {
display: flex;
width: $card-width-normal;
flex-direction: column;
overflow: hidden;
vertical-align: top;
background: $color-background;
> *:not(.navigation) {
padding: $card-offset;
}
&:not(.color) > *:not(.figure), > *:not(:last-child) {
box-shadow: 0 1px $color-divider;
}
}
align-items: center;
.touch {
cursor: pointer;
}
.avatar {
margin-right: 13px;
}
.contrast {
.figure {
color: $card-color-white;
text-shadow: 0;
.subtitle {
color: $color-text-secondary;
}
.ripple {
background-color: $card-color-white;
}
}
.loading {
pointer-events: none;
cursor: none;
filter: grayscale(100%);
.ripple {
@include ripple-loading(cardloading, 2 * $card-width-normal, 2 * $card-width-normal);
width: 2 * $card-width-normal;
height: 2 * $card-width-normal;
animation-name: cardloading;
}
}
&.large {
padding: $card-padding-lg $card-padding ($card-padding - 2px);
.image {
&, .figure {
height: $card-width-normal;
.title {
@include typo-headline();
line-height: 1.25;
}
}
.figure {
padding: 0;
> h5 {
padding: $card-offset;
font-size: $font-size-small;
font-weight: $font-weight-bold;
background-color: $card-text-overlay;
&.small {
padding: $card-padding;
.title {
@include typo-body-2(false, true);
line-height: 1.4;
}
.subtitle {
font-weight: 500;
line-height: 1.4;
}
}
}
.event {
.figure {
justify-content: flex-start;
.cardTitle, .cardText {
padding: ($card-padding - 2px) $card-padding;
&:last-child {
padding-bottom: $card-padding-lg;
}
& + .cardText {
padding-top: 0;
}
}
.wide {
width: $card-width-large;
.cardActions {
display: flex;
align-items: center;
justify-content: flex-start;
padding: $card-padding-sm $card-padding-sm;
button[data-react-toolbox="button"] {
min-width: 0;
margin: 0 $card-padding-sm/2;
&:first-child {
margin-left: $card-padding-sm;
}
&:last-child {
margin-right: $card-padding-sm;
}
}
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import ClassNames from 'classnames/bind';
/**
* Provides a component with a classNames
* function bound to the provided CSS module
*
* Check for 'classNames()' in props
*
* @param {CSS Module} styles
* @return {Component}
*/
function classNames (styles) {
const classNamesFn = ClassNames.bind(styles);
return function classNamesDecorator (ComposedComponent) {
return function ClassNamesDecor (props) {
return (
<ComposedComponent
classNames={classNamesFn}
{...props}
/>
);
};
};
}
export default classNames;

View File

@ -1,37 +1,85 @@
import React from 'react';
import Card from '../../components/card';
import Button from '../../components/button';
import {
Card,
CardActions,
CardMedia,
CardText,
CardTitle
} from '../../components/card';
import style from '../style';
const dummyText = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.';
const demos = [
{
name: 'Basic Card',
component: (
<Card className={style.card}>
<CardTitle>Basic Card</CardTitle>
<CardText>{dummyText}</CardText>
<CardActions>
<Button label="Action" />
<Button label="More" />
</CardActions>
</Card>
)
}, {
name: '16:9 Card Media',
component: (
<Card className={style.card}>
<CardMedia
aspectRatio="wide"
image="https://placeimg.com/800/450/nature"
/>
<CardTitle
title="Basic Card"
subtitle="An awesome basic card"
/>
<CardText>{dummyText}</CardText>
</Card>
)
}, {
name: 'Avatar Card Title',
component: (
<Card className={style.card}>
<CardTitle
avatar="https://placeimg.com/80/80/animals"
title="Avatar Card"
subtitle="An awesome basic card"
/>
<CardMedia
aspectRatio="wide"
image="https://placeimg.com/800/450/nature"
/>
<CardActions style={{ justifyContent: 'flex-end' }}>
<Button icon="feedback" />
<Button icon="favorite" />
</CardActions>
</Card>
)
}
];
class CardTest extends React.Component {
onClick = () => {
console.log('onClick', arguments);
};
onActionClick () {
console.log('onClick', arguments);
}
render () {
const text = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.';
const actions = [
{ label: 'Save', icon: 'add', className: 'flat accent', onClick: this.onActionClick.bind(this) },
{ label: 'Close', className: 'flat', onClick: this.onActionClick.bind(this) }];
return (
<section>
<h5>Cards</h5>
<Card title='Default Card' />
<Card title='Default Card loading' loading />
<Card type='wide' title='Wide card' />
<Card title='Default Card with text' text={text} />
<Card title='Default Card with actions' actions={actions} />
<Card title='Default Card with text and image' text={text} image='https://avatars2.githubusercontent.com/u/559654?v=3&s=460' />
<Card title='Default Card with text, image and color' text={text} color='#e91e63' image='https://avatars2.githubusercontent.com/u/559654?v=3&s=460' />
<Card title='Default Card with text, image and color' text={text} color='#00bcd4' image='https://avatars2.githubusercontent.com/u/1634922?v=3&s=460' />
<Card title='Default Card with text, color and onClick event' text={text} color='#e91e63' onClick={this.onClick} />
<Card type='wide' title='Wide Card loading with text, color and onClick event' text={text} color='#e91e63' onClick={this.onClick} loading />
<Card type='image' title='javivelasco.jpg' image='https://avatars2.githubusercontent.com/u/1634922?v=3&s=460' />
<Card type='event' title='Featured event: May 24, 2016 7-11pm' color='#00bcd4' />
</section>
<div>
<h2>Cards</h2>
<ul className={style.demoList}>
{demos.map((demo, i) => (
<li key={i} className={style.demoListItem}>
<div className={style.demo}>
{demo.component}
</div>
<div className={style.demoName}>
{demo.name}
</div>
</li>
))}
</ul>
</div>
);
}
}

View File

@ -42,26 +42,9 @@ const Root = () => (
/>
</AppBarToolbox>
<Autocomplete />
<Button />
<Card />
<Checkbox />
<Dialog />
<Drawer />
<Dropdown />
<IconMenu />
<Input />
<List />
<Menu />
<Pickers />
<Progress />
<Radio />
<Slider />
<Snackbar />
<Switch />
<Table />
<Tabs />
<Tooltip />
</App>
);

View File

@ -1,19 +1,16 @@
$import-normalize: false;
$import-flex-attributes: true;
@import "../components/commons";
@import "../components/base";
@import "../components/app_bar/config";
@import "../components/button/config";
$offset: 1.8 * $unit;
.app {
padding: (8.2 * $unit) $offset $offset;
padding: (8.2 * $unit) $offset $offset $offset;
background-color: #f5f5f5;
> section {
padding: $offset;
margin-bottom: $offset;
background-color: #fff;
background-color: white;
box-shadow: 0 1px 2px rgba(0,0,0, 0.25);
> h5 {
color: rgb(48, 63, 159);
@ -51,3 +48,42 @@ $offset: 1.8 * $unit;
}
}
}
.logo {
// height: $unit;
}
.demoList {
margin: 20px 0;
padding: 20px 0;
list-style: none;
display: flex;
position: relative;
justify-content: center;
align-items: stretch;
}
.demoListItem {
margin: 0 30px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.demo {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.demoName {
display: block;
margin: 20px 0 0;
text-align: center;
}
.card {
width: 345px;
}