diff --git a/.editorconfig b/.editorconfig index 9bb7984..521010b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,6 @@ charset = utf-8 end_of_line = lf insert_final_newline = true -[*.{js,json,html,less}] +[*.{js,json,html,scss}] indent_style = space indent_size = 2 diff --git a/index.html b/index.html index d30f63f..9bd7a88 100644 --- a/index.html +++ b/index.html @@ -5,15 +5,6 @@ Channel Hunter -
diff --git a/package.json b/package.json index 20c70fa..a84e726 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "npm": "2.14.17" }, "dependencies": { + "autoprefixer": "^6.3.6", "babel": "^6.3.26", "babel-cli": "^6.4.0", "babel-core": "^6.4.0", @@ -38,9 +39,14 @@ "babel-preset-stage-0": "^6.3.13", "body-parser": "^1.14.1", "brfs": "^1.4.3", + "classnames": "^2.2.5", "color": "^0.11.1", + "css-loader": "^0.23.1", "express": "^4.13.3", + "extract-text-webpack-plugin": "^1.0.1", "mongodb": "^2.0.49", + "node-sass": "^3.7.0", + "postcss-loader": "^0.9.1", "radium": "^0.17.1", "react": "^0.14.0", "react-addons-shallow-compare": "^0.14.7", @@ -52,6 +58,8 @@ "redux": "^3.0.4", "redux-persist": "^2.0.1", "redux-thunk": "^2.0.1", + "sass-loader": "^3.2.0", + "style-loader": "^0.13.1", "superagent": "^1.4.0", "superagent-jsonp": "0.0.6", "transform-loader": "^0.2.3", @@ -59,6 +67,7 @@ }, "devDependencies": { "babel-eslint": "^6.0.0-beta.6", + "css-modules-require-hook": "^4.0.0", "eslint": "2.2.x", "eslint-plugin-react": "^4.2.2", "expect": "^1.13.0", diff --git a/src/components/application/_theme.scss b/src/components/application/_theme.scss new file mode 100644 index 0000000..cb1e45f --- /dev/null +++ b/src/components/application/_theme.scss @@ -0,0 +1,9 @@ +$darkPrimaryColor: #303f9f; +$primaryColor: #3f51b5; +$lightPrimaryColor: #c5cae9; +$textColor: #fff; +$accentColor: #ff4081; +$primaryTextColor: #212121; +$secondaryTextColor: #727272; +$dividerColor: #b6b6b6; +$errorColor: #f44336; diff --git a/src/components/application/application-container.js b/src/components/application/application-container.js index 6aa5bd1..c1753fd 100644 --- a/src/components/application/application-container.js +++ b/src/components/application/application-container.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import HeaderContainer from '../header/header-container'; +require('./application.scss'); export default class ApplicationContainer extends Component { render() { diff --git a/src/components/application/application.scss b/src/components/application/application.scss new file mode 100644 index 0000000..b4e333d --- /dev/null +++ b/src/components/application/application.scss @@ -0,0 +1,9 @@ +:global { + body { + padding: 0; + margin: 0; + font-family: 'Roboto'; + background-color: rgba(0,0,0,.05); + -webkit-font-smoothing: antialiased; + } +} diff --git a/src/components/feed/feed-channels.js b/src/components/feed/feed-channels.js index 376f61c..73528c4 100644 --- a/src/components/feed/feed-channels.js +++ b/src/components/feed/feed-channels.js @@ -1,23 +1,22 @@ import React, {Component, PropTypes} from 'react'; import {curried} from '../../utils/common'; -import {colors} from '../../utils/styles'; import {List, ListItem, ListLabel, Avatar, Icon, Loader} from '../ui'; +import cn from 'classnames'; +import styles from './feed.scss'; export default class FeedChannels extends Component { static propTypes = { list: PropTypes.array.isRequired, onToggle: PropTypes.func.isRequired, - }; + } render() { - const styles = this.getStyles(); - const channels = this.props.list.map((channel) => { return } + leftElement={} leftElementHeight={32} rightElement={this._renderRightElement(channel)} rightElementHeight={24} @@ -25,7 +24,7 @@ export default class FeedChannels extends Component { onClick={curried(this.props.onToggle, channel)} />; }); - return
+ return
{channels}
; @@ -33,19 +32,9 @@ export default class FeedChannels extends Component { _renderRightElement(channel) { if (channel.isLoading) { - return ; + return ; } - return channel.isEnabled ? check : null; - } - - getStyles() { - return { - - channel: { - fontSize: 14, - }, - - }; + return channel.isEnabled ? : null; } } diff --git a/src/components/feed/feed-playlist.js b/src/components/feed/feed-playlist.js index 2384300..ce3e3e6 100644 --- a/src/components/feed/feed-playlist.js +++ b/src/components/feed/feed-playlist.js @@ -11,13 +11,13 @@ export default class FeedPlaylist extends Component { onToggleShuffle: PropTypes.func.isRequired, tracks: PropTypes.array.isRequired, currentTrackId: PropTypes.string, - }; + } static defaultProps = { compact: false, currentTrackId: null, isShuffle: false, - }; + } render() { const {tracks} = this.props; diff --git a/src/components/feed/feed.scss b/src/components/feed/feed.scss new file mode 100644 index 0000000..07d067c --- /dev/null +++ b/src/components/feed/feed.scss @@ -0,0 +1,13 @@ + +.channels { + &Item { + font-size: 14px; + + &Image { + width: 32px; + height: 32px; + } + } +} + + diff --git a/src/components/ui/avatar.js b/src/components/ui/avatar.js deleted file mode 100644 index 84b68af..0000000 --- a/src/components/ui/avatar.js +++ /dev/null @@ -1,30 +0,0 @@ -import React, {Component} from 'react'; - -export default class Avatar extends Component { - static propTypes = { - url: React.PropTypes.string.isRequired, - size: React.PropTypes.number, - }; - - static defaultProps = { - size: 40, - }; - - render() { - return ; - } - - getStyle() { - const {url, style, size} = this.props; - - return { - background: `url(${url}) center center`, - backgroundSize: 'cover', - display: 'inline-block', - width: size, - height: size, - borderRadius: '50%', - ...style, - }; - } -} diff --git a/src/components/ui/avatar/avatar.js b/src/components/ui/avatar/avatar.js new file mode 100644 index 0000000..9b489d9 --- /dev/null +++ b/src/components/ui/avatar/avatar.js @@ -0,0 +1,15 @@ +import React, {Component} from 'react'; +import cn from 'classnames'; +import styles from './avatar.scss'; + +export default class Avatar extends Component { + static propTypes = { + url: React.PropTypes.string.isRequired, + } + + render() { + return ; + } +} diff --git a/src/components/ui/avatar/avatar.scss b/src/components/ui/avatar/avatar.scss new file mode 100644 index 0000000..1d89f59 --- /dev/null +++ b/src/components/ui/avatar/avatar.scss @@ -0,0 +1,8 @@ +.avatar { + display: inline-block; + width: 40px; + height: 40px; + background-size: cover; + background-position: center; + border-radius: 50%; +} diff --git a/src/components/ui/button/button.scss b/src/components/ui/button/button.scss new file mode 100644 index 0000000..0429ede --- /dev/null +++ b/src/components/ui/button/button.scss @@ -0,0 +1,31 @@ +.flat { + height: 36px; + padding: 0 24px; + line-height: 16px; + font-size: 16px; + text-transform: uppercase; + border: none; + background: none; + cursor: pointer; + + &:hover { + background: rgba(0,0,0,.1); + } +} + +.icon { + display: inline-block; + position: relative; + background-color: transparent; + border: none; + outline: none; + margin: 0; + padding: 0; + cursor: pointer; + border-radius: 50%; + + &:hover { + background: rgba(0,0,0,.1); + } + +} diff --git a/src/components/ui/button/flat-button.js b/src/components/ui/button/flat-button.js new file mode 100644 index 0000000..875e774 --- /dev/null +++ b/src/components/ui/button/flat-button.js @@ -0,0 +1,15 @@ +import React, {Component, PropTypes} from 'react'; +import cn from 'classnames'; +import styles from './button.scss'; + +export default class FlatButton extends Component { + static propTypes = { + label: PropTypes.node.isRequired, + } + + render() { + return ; + } +} diff --git a/src/components/ui/button/icon-button.js b/src/components/ui/button/icon-button.js new file mode 100644 index 0000000..a1f2b81 --- /dev/null +++ b/src/components/ui/button/icon-button.js @@ -0,0 +1,23 @@ +import React, {Component, PropTypes} from 'react'; +import cn from 'classnames'; +import Icon from '../icon/icon'; +import styles from './button.scss'; + +export default class IconButton extends Component { + static propTypes = { + glyph: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + size: PropTypes.number, + boxSize: PropTypes.number, + } + + render() { + const {size, boxSize, glyph, onClick, className} = this.props; + return ; + } +} diff --git a/src/components/ui/flat-button.js b/src/components/ui/flat-button.js deleted file mode 100644 index a4bd559..0000000 --- a/src/components/ui/flat-button.js +++ /dev/null @@ -1,32 +0,0 @@ -import React, {Component, PropTypes} from 'react'; -import Radium from 'radium'; - -@Radium -export default class FlatButton extends Component { - static propTypes = { - label: PropTypes.node.isRequired, - }; - - render() { - return ; - } - - getStyles() { - return { - height: 36, - padding: '0 24px', - lineHeight: '16px', - fontSize: '16px', - textTransform: 'uppercase', - border: 'none', - background: 'none', - cursor: 'pointer', - - ':hover': { - background: 'rgba(0,0,0,.1)', - }, - - ...this.props.style, - }; - } -} diff --git a/src/components/ui/icon-button.js b/src/components/ui/icon-button.js deleted file mode 100644 index 4438a31..0000000 --- a/src/components/ui/icon-button.js +++ /dev/null @@ -1,51 +0,0 @@ -import Radium from 'radium'; -import Color from 'color'; -import React, {Component, PropTypes} from 'react'; -import Icon from './icon'; - -@Radium -export default class IconButton extends Component { - - static propTypes = { - children: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - size: PropTypes.number, - boxSize: PropTypes.number, - } - - render() { - const {size, boxSize, children, onClick} = this.props; - return ; - } - - _getIconColor() { - const style = this.props.style || {color: '#fff'}; - return style.color ? style.color : '#fff'; - } - - getStyle() { - return { - display: 'inline-block', - position: 'relative', - backgroundColor: 'transparent', - border: 'none', - outline: 'none', - margin: 0, - padding: 0, - cursor: 'pointer', - borderRadius: '50%', - - ':hover': { - backgroundColor: Color(this._getIconColor()).alpha(.1).rgbString(), - }, - - ...this.props.style, - }; - } -} diff --git a/src/components/ui/icon.js b/src/components/ui/icon.js deleted file mode 100644 index 1670864..0000000 --- a/src/components/ui/icon.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, {Component, PropTypes} from 'react'; - -export default class Icon extends Component { - static propTypes = { - children: PropTypes.string.isRequired, - size: PropTypes.number, - boxSize: PropTypes.number, - }; - - static defaultProps = { - size: 24, - boxSize: null, - }; - - render() { - return - {this.props.children} - ; - } - - getStyle() { - const {size, boxSize, style} = this.props; - - return { - display: 'inline-block', - position: 'relative', - width: (boxSize || size), - height: (boxSize || size), - lineHeight: (boxSize || size) / size, - textAlign: 'center', - fontSize: size, - color: '#fff', - verticalAlign: 'middle', - ...style, - }; - } -} diff --git a/src/components/ui/icon/icon.js b/src/components/ui/icon/icon.js new file mode 100644 index 0000000..f1e9668 --- /dev/null +++ b/src/components/ui/icon/icon.js @@ -0,0 +1,32 @@ +import React, {Component, PropTypes} from 'react'; +import cn from 'classnames'; +import styles from './icon.scss'; + +export default class Icon extends Component { + static propTypes = { + glyph: PropTypes.string.isRequired, + size: PropTypes.number, + boxSize: PropTypes.number, + }; + + static defaultProps = { + size: 24, + boxSize: null, + }; + + render() { + const {size, boxSize, glyph, className} = this.props; + + return + {glyph} + ; + } +} diff --git a/src/components/ui/icon/icon.scss b/src/components/ui/icon/icon.scss new file mode 100644 index 0000000..b7ce322 --- /dev/null +++ b/src/components/ui/icon/icon.scss @@ -0,0 +1,6 @@ +.icon { + display: inline-block; + position: relative; + text-align: center; + vertical-align: middle; +} diff --git a/src/components/ui/index.js b/src/components/ui/index.js index f67cff2..386f727 100644 --- a/src/components/ui/index.js +++ b/src/components/ui/index.js @@ -1,11 +1,11 @@ export default { - Avatar: require('./avatar'), - FlatButton: require('./flat-button'), - Icon: require('./icon'), - IconButton: require('./icon-button'), - LazyList: require('./lazylist'), - List: require('./list'), - ListItem: require('./list-item'), - ListLabel: require('./list-label'), - Loader: require('./loader'), + Avatar: require('./avatar/avatar'), + Icon: require('./icon/icon'), + FlatButton: require('./button/flat-button'), + IconButton: require('./button/icon-button'), + LazyList: require('./list/lazy-list'), + ListLabel: require('./list/list-label'), + List: require('./list/list'), + ListItem: require('./list/list-item'), + Loader: require('./loader/loader'), }; diff --git a/src/components/ui/list-item.js b/src/components/ui/list-item.js deleted file mode 100644 index f6d2414..0000000 --- a/src/components/ui/list-item.js +++ /dev/null @@ -1,122 +0,0 @@ -import React, {Component, PropTypes} from 'react'; -import Radium from 'radium'; -import {colors} from '../../utils/styles'; - -@Radium -export default class ListItem extends Component { - static propTypes = { - primaryText: PropTypes.node.isRequired, - secondaryText: PropTypes.node, - leftElement: PropTypes.node, - leftElementHeight: PropTypes.number, - rightElement: PropTypes.node, - rightElementHeight: PropTypes.number, - onClick: PropTypes.func, - style: PropTypes.object, - }; - - static defaultProps = { - secondaryText: null, - leftElement: null, - leftElementHeight: 40, - rightElement: null, - rightElementHeight: 40, - height: 56, - onClick: null, - style: null, - }; - - render() { - const styles = this.getStyles(); - const {leftElement, rightElement} = this.props; - - return
  • - {this.renderElement(leftElement, styles.leftElement)} - {this.props.primaryText} - {this.props.secondaryText} - {this.renderElement(rightElement, styles.rightElement)} -
  • ; - } - - renderElement(element, elementStyle) { - if (element === null) { - return null; - } - - const style = Object.assign({}, elementStyle, element.props.style); - - return React.cloneElement( - element, - {...element.props, style} - ); - } - - _click(event) { - if (this.props.onClick) { - this.props.onClick(event); - } - } - - getStyles() { - const height = this.props.height; - const paddingVert = height/2 - (this.props.secondaryText ? 16 : 8); - const paddingRight = this.props.rightIcon === null ? 16 : 56; - const paddingLeft = this.props.leftElement === null ? 16 : 72; - const isClickable = typeof(this.props.onClick) === 'function'; - const ellipsis = { - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - }; - - return { - container: { - position: 'relative', - height, - boxSizing: 'border-box', - padding: `${paddingVert}px ${paddingRight}px ${paddingVert}px ${paddingLeft}px`, - lineHeight: 1.1, - fontSize: 16, - cursor: isClickable ? 'pointer' : null, - ':hover': isClickable ? { - backgroundColor: 'rgba(0,0,0,.05)', - } : null, - ...this.props.style, - }, - - primaryText: { - display: 'inline-block', - width: '100%', - ...ellipsis, - }, - - secondaryText: { - display: 'inline-block', - width: '100%', - fontSize: 14, - color: colors.secondaryText, - ...ellipsis, - }, - - leftElement: { - position: 'absolute', - left: 16, - top: this._getSideElementTop(height, this.props.leftElementHeight), - color: colors.primaryText, - }, - - rightElement: { - position: 'absolute', - right: 16, - top: this._getSideElementTop(height, this.props.rightElementHeight), - color: colors.primaryText, - }, - }; - } - - _getSideElementTop(itemHeight, elementHeight) { - return ((itemHeight - elementHeight) / 2) + 'px'; - } -} diff --git a/src/components/ui/list-label.js b/src/components/ui/list-label.js deleted file mode 100644 index b9e0aee..0000000 --- a/src/components/ui/list-label.js +++ /dev/null @@ -1,74 +0,0 @@ -import React, {Component} from 'react'; -import {colors} from '../../utils/styles'; - -export default class ListLabel extends Component { - static propTypes = { - text: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.element, - ]).isRequired, - rightElement: React.PropTypes.element, - }; - - static defaultProps = { - rightElement: null, - }; - - render() { - const styles = this.getStyles(); - - return
    - {this.props.text} - {this.renderElement(this.props.rightElement, styles.rightElement)} -
    ; - } - - renderElement(element, elementStyle) { - if (element === null) { - return null; - } - - const style = Object.assign({}, elementStyle, element.props.style); - - return React.cloneElement( - element, - {...element.props, style} - ); - } - - getStyles() { - - const ellipsis = { - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - }; - - return { - - container: { - position: 'relative', - boxSizing: 'border-box', - padding: '0 16px', - height: '48px', - }, - - text: { - display: 'inline-block', - width: '100%', - fontSize: '14px', - lineHeight: '48px', - color: colors.secondaryText, - ...ellipsis, - }, - - rightElement: { - position: 'absolute', - right: '16px', - top: '12px', - color: colors.secondaryText, - }, - - }; - } -} diff --git a/src/components/ui/list.js b/src/components/ui/list.js deleted file mode 100644 index e262057..0000000 --- a/src/components/ui/list.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, {Component, PropTypes} from 'react'; - -export default class List extends Component { - static propTypes = { - children: PropTypes.node.isRequired, - }; - - render() { - return
      {this.props.children}
    ; - } - - getStyle() { - return { - listStyleType: 'none', - margin: 0, - padding: 0, - ...this.props.style, - }; - } -} diff --git a/src/components/ui/lazylist.js b/src/components/ui/list/lazy-list.js similarity index 89% rename from src/components/ui/lazylist.js rename to src/components/ui/list/lazy-list.js index cc78d85..06d6757 100644 --- a/src/components/ui/lazylist.js +++ b/src/components/ui/list/lazy-list.js @@ -1,6 +1,8 @@ import React, {Component, PropTypes} from 'react'; import ReactDOM from 'react-dom'; -import {nodeOffset, throttle} from '../../utils/common'; +import {nodeOffset, throttle} from '../../../utils/common'; +import cn from 'classnames'; +import styles from './list.scss'; export default class LazyList extends Component { @@ -29,9 +31,10 @@ export default class LazyList extends Component { render() { const {items, renderItem} = this.props; const {firstIndex, lastIndex} = this.state; - const styles = this.getStyles(); + const offsetStyles = this._getOffsetStyles(); + const className = cn(styles.list, this.props.className); - return
      + return
        {items.slice(firstIndex, lastIndex).map(renderItem)}
      ; } @@ -117,19 +120,10 @@ export default class LazyList extends Component { this.setState(this._computeState()); } - getStyles() { + _getOffsetStyles() { return { - - container: { - listStyleType: 'none', - boxSizing: 'border-box', - margin: 0, - padding: 0, - paddingTop: this.state.bufferStart, - height: this.state.height, - ...this.props.style, - }, - + paddingTop: this.state.bufferStart, + height: this.state.height, }; } } diff --git a/src/components/ui/list/list-item.js b/src/components/ui/list/list-item.js new file mode 100644 index 0000000..dfb79b9 --- /dev/null +++ b/src/components/ui/list/list-item.js @@ -0,0 +1,90 @@ +import React, {Component, PropTypes} from 'react'; +import cn from 'classnames'; +import styles from './list.scss'; + +export default class ListItem extends Component { + static propTypes = { + primaryText: PropTypes.node.isRequired, + secondaryText: PropTypes.node, + leftElement: PropTypes.node, + leftElementHeight: PropTypes.number, + rightElement: PropTypes.node, + rightElementHeight: PropTypes.number, + onClick: PropTypes.func, + }; + + static defaultProps = { + secondaryText: null, + leftElement: null, + leftElementHeight: 40, + rightElement: null, + rightElementHeight: 40, + height: 56, + onClick: null, + style: null, + }; + + render() { + const {leftElement, rightElement, className} = this.props; + const itemStyles = this._getStyles(); + + return
    • + {this.renderElement(leftElement, styles.itemLeftElement, itemStyles.leftElement)} + {this.props.primaryText} + {this.props.secondaryText} + {this.renderElement(rightElement, styles.itemRightElement, itemStyles.rightElement)} +
    • ; + } + + renderElement(element, className, style) { + if (element === null) { + return null; + } + + return React.cloneElement( + element, + { + ...element.props, + className: cn(element.props.className, className), + style: {...element.props.style, ...style}, + } + ); + } + + _getStyles() { + const height = this.props.height; + const paddingVert = height/2 - (this.props.secondaryText ? 16 : 8); + const paddingRight = this.props.rightIcon === null ? 16 : 56; + const paddingLeft = this.props.leftElement === null ? 16 : 72; + + return { + item: { + height, + padding: `${paddingVert}px ${paddingRight}px ${paddingVert}px ${paddingLeft}px`, + }, + + leftElement: { + top: getSideElementTop(height, this.props.leftElementHeight), + }, + + rightElement: { + top: getSideElementTop(height, this.props.rightElementHeight), + }, + }; + } + + _click = (event) => { + if (this.props.onClick) { + this.props.onClick(event, this.props.key); + } + } +} + +function getSideElementTop(itemHeight, elementHeight) { + return ((itemHeight - elementHeight) / 2) + 'px'; +} diff --git a/src/components/ui/list/list-label.js b/src/components/ui/list/list-label.js new file mode 100644 index 0000000..f03a70f --- /dev/null +++ b/src/components/ui/list/list-label.js @@ -0,0 +1,44 @@ +import React, {Component} from 'react'; +import cn from 'classnames'; +import styles from './list.scss'; + +export default class ListLabel extends Component { + static propTypes = { + text: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element, + ]).isRequired, + rightElement: React.PropTypes.element, + } + + static defaultProps = { + rightElement: null, + } + + render() { + return
      + {this.props.text} + {this._renderElement(this.props.rightElement)} +
      ; + } + + _renderElement(element) { + if (element === null) { + return null; + } + + const className = cn( + element.props.className, + styles.labelRightElement + ); + + return React.cloneElement( + element, + { + ...element.props, + className, + } + ); + } + +} diff --git a/src/components/ui/list/list.js b/src/components/ui/list/list.js new file mode 100644 index 0000000..499f64d --- /dev/null +++ b/src/components/ui/list/list.js @@ -0,0 +1,15 @@ +import React, {Component, PropTypes} from 'react'; +import cn from 'classnames'; +import styles from './list.scss'; + +export default class List extends Component { + static propTypes = { + children: PropTypes.node.isRequired, + } + + render() { + return
        + {this.props.children} +
      ; + } +} diff --git a/src/components/ui/list/list.scss b/src/components/ui/list/list.scss new file mode 100644 index 0000000..edf5f3f --- /dev/null +++ b/src/components/ui/list/list.scss @@ -0,0 +1,79 @@ +@import '../../application/_theme.scss'; + +@mixin ellipsis { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.list { + list-style-type: none; + box-sizing: border-box; + margin: 0; + padding: 0; +} + +.item { + position: relative; + box-sizing: border-box; + line-height: 1.1; + font-size: 16px; + + &PrimaryText { + display: inline-block; + width: 100%; + @include ellipsis; + } + + &SecondaryText { + display: inline-block; + width: 100%; + font-size: 0.9em; + @include ellipsis; + } + + @mixin sideElement { + position: absolute; + color: $primaryTextColor; + } + + &LeftElement { + left: 16px; + @include sideElement; + } + + &RightElement { + right: 16px; + @include sideElement; + } + + &Interactive { + cursor: pointer; + + &:hover { + background-color: rgba(0,0,0,.05); + } + } +} + +.label { + position: relative; + height: 48px; + padding: 0 16px; + box-sizing: border-box; + + &Text { + display: inline-block; + width: 100%; + font-size: 14px; + line-height: 48px; + color: $secondaryTextColor; + @include ellipsis; + } + + &RightElement { + position: absolute; + right: 16px; + top: 12px; + } +} diff --git a/src/components/ui/loader.js b/src/components/ui/loader.js deleted file mode 100644 index 038a284..0000000 --- a/src/components/ui/loader.js +++ /dev/null @@ -1,92 +0,0 @@ -import React, {Component, PropTypes} from 'react'; -import Radium from 'radium'; - -@Radium -export default class Loader extends Component { - - static propTypes = { - color: PropTypes.string, - strokeWidth: PropTypes.number, - size: PropTypes.number, - }; - - static defaultProps = { - color: '#fff', - strokeWidth: 4, - size: 24, - }; - - render() { - const {strokeWidth} = this.props; - const styles = this.getStyles(); - - return
      - - - -
      ; - } - - getStyles() { - const {size, color, style} = this.props; - - return { - container: { - position: 'relative', - width: size, - height: size, - ...style, - }, - - circular: { - height: '100%', - transformOrigin: 'center center', - width: '100%', - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - margin: 'auto', - animation: 'x 2s linear infinite', - animationName: loaderRotateAnimation, - }, - - path: { - strokeDasharray: '1,200', - strokeDashoffset: 0, - strokeLinecap: 'round', - stroke: color, - animation: 'x 1.5s ease-in-out infinite', - animationName: loaderDashAnimation, - }, - }; - } -} - -const loaderRotateAnimation = Radium.keyframes({ - '100%': { - transform: 'rotate(360deg)', - }, -}); - -const loaderDashAnimation = Radium.keyframes({ - '0%': { - strokeDasharray: '1,200', - strokeDashoffset: 0, - }, - '50%': { - strokeDasharray: '89,200', - strokeDashoffset: '-35px', - }, - '100%': { - strokeDasharray: '89,200', - strokeDashoffset: '-124px', - }, -}); diff --git a/src/components/ui/loader/loader.js b/src/components/ui/loader/loader.js new file mode 100644 index 0000000..dc8ac04 --- /dev/null +++ b/src/components/ui/loader/loader.js @@ -0,0 +1,38 @@ +import React, {Component, PropTypes} from 'react'; +import cn from 'classnames'; +import styles from './loader.scss'; + +export default class Loader extends Component { + static propTypes = { + contrast: PropTypes.bool, + strokeWidth: PropTypes.number, + size: PropTypes.number, + } + + static defaultProps = { + strokeWidth: 4, + size: 24, + contrast: false, + } + + render() { + const {size, strokeWidth, contrast, className} = this.props; + + return
      + + + +
      ; + } +} diff --git a/src/components/ui/loader/loader.scss b/src/components/ui/loader/loader.scss new file mode 100644 index 0000000..ac21ed4 --- /dev/null +++ b/src/components/ui/loader/loader.scss @@ -0,0 +1,56 @@ +@import '../../application/_theme.scss'; + +.loader { + position: relative; + + &Circle { + height: 100%; + transform-origin: center center; + width: 100%; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + animation: circle 2s linear infinite; + } + + &Path { + stroke-dasharray: 1,200; + stroke-dashoffset: 0; + stroke-linecap: round; + stroke: $primaryTextColor; + animation: dash 1.7s ease-in-out infinite; + + &Contrast { + stroke: $textColor; + } + } + +} + +@keyframes circle { + 100% { + transform: rotate(360deg); + } +} + +@keyframes dash { + 0% { + stroke-dasharray: 1,200; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 130,200; + stroke-dashoffset: 0; + } + 65% { + stroke-dasharray: 110,200; + stroke-dashoffset: -25px; + } + 100% { + stroke-dasharray: 89,200; + stroke-dashoffset: -124px; + } +} diff --git a/src/reducers/feed.js b/src/reducers/feed.js index c5ff6e0..1a3ec61 100644 --- a/src/reducers/feed.js +++ b/src/reducers/feed.js @@ -103,6 +103,12 @@ const handlers = { }, [REHYDRATE]: (state, action) => { + const {payload} = action; + + if (!payload || !payload.feed || !payload.feed.channels) { + return state; + } + let restoredChannels = action.payload.feed.channels.map((c) => { return {...c, isLoading: false, isLoaded: false}; }); diff --git a/src/store.js b/src/store.js index 56acebd..e1e813c 100644 --- a/src/store.js +++ b/src/store.js @@ -17,6 +17,7 @@ persistStore(store, { whitelist: ['feed'], transforms: [{ in: (state) => { + console.log(state); return {channels: state.channels}; }, out: (x) => x, diff --git a/test/components/ui/lazylist_test.js b/test/components/ui/lazylist_test.js index f71296b..48fd19c 100644 --- a/test/components/ui/lazylist_test.js +++ b/test/components/ui/lazylist_test.js @@ -1,5 +1,5 @@ import React from 'react'; -import LazyList from '../../../src/components/ui/lazylist'; +import LazyList from '../../../src/components/ui/list/lazy-list'; import {renderDOM} from '../../utils'; describe('LazyList component', () => { diff --git a/test/components/ui/list-item_test.js b/test/components/ui/list-item_test.js index 3cb9943..1ea1659 100644 --- a/test/components/ui/list-item_test.js +++ b/test/components/ui/list-item_test.js @@ -1,6 +1,7 @@ import React from 'react'; -import ListItem from '../../../src/components/ui/list-item'; +import ListItem from '../../../src/components/ui/list/list-item'; import TestUtils from 'react-addons-test-utils'; +import styles from '../../../src/components/ui/list/list.scss'; import {findWithClass} from 'react-shallow-testutils'; import {renderDOM, shallowRender} from '../../utils'; @@ -107,18 +108,12 @@ describe('ListItem component', () => { TestUtils.Simulate.click(dom); }); - it('changes background when mouseEnter, if onClick is provided', () => { + it('has interactive className, if onClick prop is not empty', () => { const dom = renderDOM( null} /> ); - const leaveColor = dom.style.getPropertyValue('background-color'); - - TestUtils.Simulate.mouseEnter(dom); - expect(dom.style.getPropertyValue('background-color')).toNotBe(leaveColor); - - TestUtils.Simulate.mouseLeave(dom); - expect(dom.style.getPropertyValue('background-color')).toBe(leaveColor); + expect(dom.getAttribute('class')).toContain(styles.itemInteractive); }); it('not changes background on mouseEnter, if onClick is not provided', () => { diff --git a/test/reducers/feed_test.js b/test/reducers/feed_test.js index 2da10e5..b4ffa34 100644 --- a/test/reducers/feed_test.js +++ b/test/reducers/feed_test.js @@ -225,4 +225,34 @@ describe('Feed reducer', () => { ]); }); + + it('ignores invalid state on rehydrate', () => { + const initialState = { + channels: [ + {id: 1, isEnabled: false, isLoaded: false, isLoading: false}, + {id: 2, isEnabled: false, isLoaded: false, isLoading: false}, + ], + }; + + expect(reducer(initialState, { + type: REHYDRATE, + payload: null, + })).toBe(initialState); + + expect(reducer(initialState, { + type: REHYDRATE, + payload: {}, + })).toBe(initialState); + + expect(reducer(initialState, { + type: REHYDRATE, + payload: {feed: null}, + })).toBe(initialState); + + expect(reducer(initialState, { + type: REHYDRATE, + payload: {feed: {channels: null}}, + })).toBe(initialState); + }); + }); diff --git a/test/setup.js b/test/setup.js index 97143b8..521a5d5 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,5 +1,13 @@ -import jsdom from 'jsdom'; import expect from 'expect'; +import jsdom from 'jsdom'; +import cssHook from 'css-modules-require-hook'; +import sass from 'node-sass'; + +cssHook({ + extensions: ['.scss'], + generateScopedName: '[local]___[hash:base64:5]', + preprocessCss: (data, file) => sass.renderSync({file}).css, +}); global.document = jsdom.jsdom(''); global.window = document.defaultView; diff --git a/webpack.config.dev.js b/webpack.config.dev.js index 8e03813..217c5a8 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -1,5 +1,6 @@ var path = require('path'); var webpack = require('webpack'); +var autoprefixer = require('autoprefixer'); module.exports = { cache: true, @@ -48,6 +49,18 @@ module.exports = { ], }, }, + { + test: /\.(scss|css)$/, + exclude: /node_modules/, + include: path.join(__dirname, 'src'), + loaders: [ + 'style?singleton', + 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + 'postcss', + 'sass', + ], + }, ], }, + postcss: [autoprefixer], }; diff --git a/webpack.config.prod.js b/webpack.config.prod.js index d32d300..5a11667 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -1,8 +1,10 @@ var path = require('path'); var webpack = require('webpack'); +var ExtractTextPlugin = require('extract-text-webpack-plugin'); +var autoprefixer = require('autoprefixer'); module.exports = { - devtool: 'source-map', + devtool: 'eval', entry: [ 'babel-polyfill', './src/client', @@ -13,6 +15,7 @@ module.exports = { publicPath: '/assets/', }, plugins: [ + new ExtractTextPlugin('style.css', {allChunks: true}), new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin({ 'process.env': { @@ -40,6 +43,16 @@ module.exports = { include: path.join(__dirname, 'src'), loaders: ['babel-loader'], }, + { + test: /\.(scss|css)$/, + exclude: /node_modules/, + include: path.join(__dirname, 'src'), + loader: ExtractTextPlugin.extract( + 'style-loader', + 'css-loader?modules&importLoaders=1&localIdentName=[hash:base64:5]!postcss-loader!sass-loader' + ), + }, ], }, + postcss: [autoprefixer], };