├── .babelrc ├── .editorconfig ├── .gitignore ├── .gitmodules ├── README.md ├── actions ├── filter.js ├── index.js └── todos.js ├── client.js ├── components ├── DebugAnimationComponent.js ├── Filter.js ├── Input.js ├── Scrub.js ├── TodoItem.js └── TodoList.js ├── containers ├── App.js ├── TimeTraveler.js └── TodoApp.js ├── demo.js ├── index.html ├── package.json ├── postcss.config.js ├── reducers ├── filter.js ├── index.js └── todos.js ├── server.js ├── static └── .gitkeep ├── store ├── action-history.js ├── history.js └── index.js ├── style ├── blue-theme.less ├── index.js ├── mint-theme.less ├── range.less ├── red-theme.less ├── style.less └── styleguide │ ├── .gitignore │ ├── colors.less │ ├── core.less │ ├── media.less │ ├── reset.less │ ├── themes │ ├── default.less │ ├── friends-and-foes.less │ ├── ocean-sunset.less │ └── phaedra.less │ └── units.less └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", {"modules": false}], 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": ["react-hot-loader/babel"] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "static/styleguide"] 2 | path = static/styleguide 3 | url = git@github.com:evanrs/styleguide.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # use-redux 2 | A project using redux, learning by doing 3 | -------------------------------------------------------------------------------- /actions/filter.js: -------------------------------------------------------------------------------- 1 | export const FILTER_NONE = 'FILTER_NONE'; 2 | export const FILTER_COMPLETE = 'FILTER_COMPLETE'; 3 | export const FILTER_INCOMPLETE = 'FILTER_INCOMPLETE'; 4 | 5 | export function filterNone() { 6 | return {type: FILTER_NONE} 7 | } 8 | 9 | export function filterComplete() { 10 | return {type: FILTER_COMPLETE} 11 | } 12 | 13 | export function filterIncomplete() { 14 | return {type: FILTER_INCOMPLETE} 15 | } 16 | -------------------------------------------------------------------------------- /actions/index.js: -------------------------------------------------------------------------------- 1 | import * as filter from './filter'; 2 | import * as todos from './todos'; 3 | 4 | export default {filter, todos}; 5 | -------------------------------------------------------------------------------- /actions/todos.js: -------------------------------------------------------------------------------- 1 | export const DRAFT_TODO = 'DRAFT_TODO'; 2 | export const ADD_TODO = 'ADD_TODO'; 3 | export const EDIT_TODO = 'EDIT_TODO'; 4 | export const SAVE_TODO = 'SAVE_TODO'; 5 | export const TOGGLE_TODO = 'TOGGLE_TODO'; 6 | export const REMOVE_TODO = 'REMOVE_TODO'; 7 | 8 | export function draftTodo(id, text) { 9 | return {type: DRAFT_TODO, id, text}; 10 | } 11 | 12 | export function addTodo(id, text) { 13 | return {type: ADD_TODO, id, text}; 14 | } 15 | 16 | export function editTodo(id, text) { 17 | return {type: EDIT_TODO, id}; 18 | } 19 | 20 | export function saveTodo(id, text) { 21 | return {type: SAVE_TODO, id, text} 22 | } 23 | 24 | export function toggleTodo (id) { 25 | return {type: TOGGLE_TODO, id} 26 | } 27 | 28 | export function removeTodo(id) { 29 | return {type: REMOVE_TODO, id} 30 | } 31 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | require('./style'); 2 | 3 | if (! localStorage.getItem('_actionHistory') || 4 | /reset/g.test(location.search)) { 5 | localStorage.setItem('_actionHistory', JSON.stringify(require('./demo'))); 6 | } 7 | 8 | Promise.all([ 9 | import('react'), 10 | import('react-dom'), 11 | import('react-tap-event-plugin'), 12 | import('./containers/App') 13 | ]).then(([React, ReactDOM, injectTapEventPlugin, { default: App }]) => { 14 | injectTapEventPlugin(); 15 | 16 | ReactDOM.render( 17 | React.createElement(App), 18 | document.getElementById('root')); 19 | }) 20 | -------------------------------------------------------------------------------- /components/DebugAnimationComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class DebugAnimationComponent extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {toggle: true}; 7 | const toggleState = () => this.setState({toggle: ! this.state.toggle}) 8 | global.setInterval(() => { 9 | toggleState() 10 | global.setTimeout(toggleState, 30) 11 | }, 2000); 12 | 13 | var render = this.render; 14 | this.render = () => this.state.toggle ? render() : ; 15 | } 16 | } 17 | 18 | export default DebugAnimationComponent; 19 | 20 | export class ToggleAnimationComponent extends Component { 21 | constructor(props) { 22 | super(props); 23 | 24 | this.state = {_toggleAnimation: true, ...this.state}; 25 | 26 | this._render = this.render; 27 | this.render = this.toggleAnimationRender; 28 | 29 | this._toggleAnimationInterval = global.setInterval(() => { 30 | this.toggleAnimationToggle(); 31 | global.setTimeout(this.toggleAnimationToggle.bind(this), 300) 32 | }, 2000); 33 | } 34 | 35 | componentWillUnmount() { 36 | global.clearInterval(this._toggleAnimationInterval) 37 | } 38 | 39 | toggleAnimationToggle() { 40 | this.setState({_toggleAnimation: ! this.state._toggleAnimation}) 41 | } 42 | 43 | toggleAnimationRender() { 44 | return this.state._toggleAnimation ? this._render() : ; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /components/Filter.js: -------------------------------------------------------------------------------- 1 | import map from 'lodash/map'; 2 | import React, { Component, PropTypes } from 'react'; 3 | 4 | import { 5 | FILTER_NONE, 6 | FILTER_COMPLETE, 7 | FILTER_INCOMPLETE } from '../actions/filter'; 8 | 9 | const FILTER_TITLES = { 10 | [FILTER_NONE]: 'all', 11 | [FILTER_COMPLETE]: 'complete', 12 | [FILTER_INCOMPLETE]: 'incomplete' 13 | } 14 | 15 | export default class Filter extends Component { 16 | render() { 17 | let { current, onFilter } = this.props; 18 | return ( 19 | 20 | {map(FILTER_TITLES, (displayName, FILTER) => ( 21 | onFilter(FILTER)}> 26 | {displayName} 27 | 28 | ))} 29 | 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/Input.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import debounce from 'lodash/debounce'; 3 | 4 | export default class Input extends Component { 5 | componentWillMount() { 6 | this.onInput = debounce(() => this.props.onInput(this.state.value), 185); 7 | this.componentWillReceiveProps(this.props); 8 | } 9 | 10 | componentWillReceiveProps(props) { 11 | this.onInput.cancel(); 12 | this.setState({value: props.value}); 13 | } 14 | 15 | componentDidMount() { 16 | this.handleUpdate(); 17 | } 18 | 19 | componentDidUpdate() { 20 | this.handleUpdate(); 21 | } 22 | 23 | handleUpdate() { 24 | // Move caret to end of string on text replace 25 | if (this.state.value === this.props.value) { 26 | let input = this.input; 27 | let caret = input.value.length; 28 | if (caret > 0 && input.selectionStart === 0) { 29 | input.setSelectionRange(caret, caret); 30 | } 31 | } 32 | } 33 | 34 | focus() { 35 | this.input.focus(); 36 | } 37 | 38 | render() { 39 | return ( 40 |
{ 42 | event.preventDefault(); 43 | event.stopPropagation(); 44 | this.onInput.cancel(); 45 | this.props.onSubmit(this.state.value); 46 | }} 47 | > 48 | this.input = elm} 50 | type="text" 51 | value={this.state.value} 52 | onChange={event => { 53 | event.preventDefault(); 54 | event.stopPropagation(); 55 | let {value} = event.target; 56 | 57 | // Record on every word 58 | ! /\b$/.test(value) && ! /\s\s+$/.test(value) ? 59 | this.props.onInput(value) : this.onInput(value); 60 | 61 | this.setState({value}); 62 | }} 63 | style={{width: '100%'}} 64 | /> 65 |
66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /components/Scrub.js: -------------------------------------------------------------------------------- 1 | import after from 'lodash/after'; 2 | import throttle from 'lodash/throttle'; 3 | import React, { Component, PropTypes } from 'react'; 4 | 5 | export default class Scrub extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = {}; 10 | this.scrub = this.scrub.bind(this); 11 | } 12 | 13 | scrub() { 14 | this.state.scrubing ? 15 | this.props.onScrub() : global.clearInterval(this.interval); 16 | } 17 | 18 | start(touch) { 19 | if (touch) this.touch = true 20 | 21 | if (this.touch && ! touch) return; 22 | 23 | if (! this.state.scrubing) { 24 | this.setState({scrubing: true}); 25 | this.props.onScrub(); 26 | } 27 | 28 | global.clearInterval(this.interval); 29 | this.interval = global.setInterval(after(10, throttle(this.scrub, 90)), 29); 30 | } 31 | 32 | stop(touch) { 33 | if (this.touch && ! touch) return; 34 | 35 | this.setState({scrubing: false}); 36 | global.clearInterval(this.interval); 37 | } 38 | 39 | render() { 40 | return ( 41 | this.start()} 43 | onMouseUp={e => this.stop()} 44 | onTouchStart={e => this.start(true)} 45 | onTouchCancel={e => this.stop(true)} 46 | onTouchEnd={e => this.stop(true)} 47 | > 48 | {this.props.children} 49 | 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import CSSTransitionGroup from 'react-addons-css-transition-group'; 3 | 4 | export default class TodoItem extends Component { 5 | state = { 6 | 7 | }; 8 | 9 | componentWillReceiveProps(nextProps) { 10 | this.setState({active: void 0}); 11 | } 12 | 13 | render() { 14 | const { item, onToggle, onDelete } = this.props; 15 | 16 | let active = 17 | this.state.active !== void 0 ? 18 | this.state.active : item.complete; 19 | 20 | return ( 21 |
22 | 28 |
29 |
( 32 | this.setState({active: ! item.complete}), 33 | setTimeout(() => onToggle(item.id), 25) 34 | )} 35 | > 36 | 37 | 38 | 39 |
40 |
41 | {item.text} 42 |
43 | onDelete(item.id)}> 46 |  ×  47 | 48 |
49 |
50 |
51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import CSSTransitionGroup from 'react-addons-css-transition-group'; 3 | 4 | import matches from 'lodash/matches'; 5 | 6 | import TodoItem from './TodoItem'; 7 | 8 | export default class TodoList extends Component { 9 | static propTypes = { 10 | todos: PropTypes.array, 11 | onToggle: React.PropTypes.func.isRequired, 12 | onDelete: React.PropTypes.func.isRequired 13 | } 14 | 15 | static defaultProps = { 16 | todos: [] 17 | } 18 | 19 | render() { 20 | let {items, filter, filter: {test}} = this.props; 21 | 22 | items = items.reverse(); 23 | test = x => matches(filter.test)(x); 24 | 25 | let active = items.filter(test); 26 | let disabled = items.filterNot(test); 27 | 28 | return ( 29 |
30 | 36 | 37 | {active.map(this.renderItem)} 38 | 39 |
40 |
41 |
42 | 43 | {disabled.map(this.renderItem)} 44 |
45 |
46 | ) 47 | } 48 | 49 | renderItem = (item) => 50 | 51 | } 52 | -------------------------------------------------------------------------------- /containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSTransitionGroup from 'react-addons-css-transition-group'; 3 | 4 | import { Provider } from 'react-redux'; 5 | import TodoApp from './TodoApp'; 6 | import TimeTraveler from './TimeTraveler'; 7 | import { AppContainer } from 'react-hot-loader'; 8 | 9 | import createStore from '../store'; 10 | 11 | const store = createStore(); 12 | 13 | class App extends Component { 14 | render() { 15 | return ( 16 | 17 | 18 | 19 |
20 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /containers/TimeTraveler.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSTransitionGroup from 'react-addons-css-transition-group'; 3 | import { connect } from 'react-redux'; 4 | 5 | import actions from '../actions'; 6 | 7 | class HistoryApp extends Component { 8 | 9 | render() { 10 | const { dispatch, cursor, size } = this.props; 11 | 12 | return ( 13 | 20 |
21 |
scrubber
22 | { 26 | dispatch({type: '@@GOTO', cursor: size - event.target.value}) 27 | }} 28 | /> 29 |
30 |
31 | ) 32 | } 33 | } 34 | 35 | function mapState({cursor, historySize}) { 36 | return {cursor, size: historySize}; 37 | } 38 | 39 | function mapDispatch(dispatch) { 40 | return {dispatch} 41 | } 42 | 43 | export default connect(mapState, mapDispatch)(HistoryApp); 44 | -------------------------------------------------------------------------------- /containers/TodoApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import difference from 'lodash/difference'; 5 | import debounce from 'lodash/debounce'; 6 | 7 | import Input from '../components/Input'; 8 | import TodoList from '../components/TodoList'; 9 | import Filter from '../components/Filter'; 10 | import Scrub from '../components/Scrub'; 11 | 12 | import actions from '../actions'; 13 | 14 | class TodoApp extends Component { 15 | static style = { 16 | maxWidth: 320, 17 | margin: '0 auto' 18 | } 19 | 20 | render() { 21 | const { actions, dispatch, draft, items, filter } = this.props; 22 | const { text, id } = draft || {}; 23 | 24 | return ( 25 |
26 |
27 |

todo

28 |
29 | actions.draftTodo(id, text)} 32 | onSubmit={(text='') => { 33 | actions.draftTodo(id, text); 34 | text.length && actions.addTodo(id, text) 35 | }} 36 | /> 37 |
38 | dispatch({type: '@@UNDO'})}> 39 | 40 | 41 | dispatch({type})}/> 42 | dispatch({type: '@@REDO'})}> 43 | 44 | 45 |
46 | 54 |
55 | ) 56 | } 57 | } 58 | 59 | function mapState({todos, filter}) { 60 | let draft = todos.get('draft'); 61 | let items = todos.get('items'); 62 | 63 | return { 64 | draft, 65 | items, 66 | filter 67 | } 68 | } 69 | 70 | function mapDispatch(dispatch) { 71 | return { 72 | actions: bindActionCreators(actions.todos, dispatch), 73 | dispatch 74 | } 75 | } 76 | 77 | export default connect(mapState, mapDispatch)(TodoApp); 78 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "type": "FILTER_COMPLETE" 4 | }, 5 | { 6 | "type": "FILTER_INCOMPLETE" 7 | }, 8 | { 9 | "type": "TOGGLE_TODO", 10 | "id": 8 11 | }, 12 | { 13 | "type": "ADD_TODO", 14 | "id": 9, 15 | "text": "Share with friends" 16 | }, 17 | { 18 | "type": "DRAFT_TODO", 19 | "id": 9, 20 | "text": "Share with friends" 21 | }, 22 | { 23 | "type": "DRAFT_TODO", 24 | "id": 9, 25 | "text": "Share with friends" 26 | }, 27 | { 28 | "type": "DRAFT_TODO", 29 | "id": 9, 30 | "text": "Share with friends" 31 | }, 32 | { 33 | "type": "DRAFT_TODO", 34 | "id": 9, 35 | "text": "Share with fr" 36 | }, 37 | { 38 | "type": "DRAFT_TODO", 39 | "id": 9, 40 | "text": "Share with " 41 | }, 42 | { 43 | "type": "DRAFT_TODO", 44 | "id": 9, 45 | "text": "Share " 46 | }, 47 | { 48 | "type": "DRAFT_TODO", 49 | "id": 9, 50 | "text": "S" 51 | }, 52 | { 53 | "type": "ADD_TODO", 54 | "id": 8, 55 | "text": "Add gratuitous time travel scrubber" 56 | }, 57 | { 58 | "type": "DRAFT_TODO", 59 | "id": 8, 60 | "text": "Add gratuitous time travel scrubber" 61 | }, 62 | { 63 | "type": "DRAFT_TODO", 64 | "id": 8, 65 | "text": "Add gratuitous time travel scru" 66 | }, 67 | { 68 | "type": "DRAFT_TODO", 69 | "id": 8, 70 | "text": "Add gratuitous time travel sc" 71 | }, 72 | { 73 | "type": "DRAFT_TODO", 74 | "id": 8, 75 | "text": "Add gratuitous time travel " 76 | }, 77 | { 78 | "type": "DRAFT_TODO", 79 | "id": 8, 80 | "text": "Add gratuitous time tra" 81 | }, 82 | { 83 | "type": "DRAFT_TODO", 84 | "id": 8, 85 | "text": "Add gratuitous time tr" 86 | }, 87 | { 88 | "type": "DRAFT_TODO", 89 | "id": 8, 90 | "text": "Add gratuitous time " 91 | }, 92 | { 93 | "type": "DRAFT_TODO", 94 | "id": 8, 95 | "text": "Add gratuitous time" 96 | }, 97 | { 98 | "type": "DRAFT_TODO", 99 | "id": 8, 100 | "text": "Add gratuitous " 101 | }, 102 | { 103 | "type": "DRAFT_TODO", 104 | "id": 8, 105 | "text": "Add gratuit" 106 | }, 107 | { 108 | "type": "DRAFT_TODO", 109 | "id": 8, 110 | "text": "Add gratu" 111 | }, 112 | { 113 | "type": "DRAFT_TODO", 114 | "id": 8, 115 | "text": "Add grat" 116 | }, 117 | { 118 | "type": "DRAFT_TODO", 119 | "id": 8, 120 | "text": "Add gra" 121 | }, 122 | { 123 | "type": "DRAFT_TODO", 124 | "id": 8, 125 | "text": "Add " 126 | }, 127 | { 128 | "type": "DRAFT_TODO", 129 | "id": 8, 130 | "text": "Add" 131 | }, 132 | { 133 | "type": "DRAFT_TODO", 134 | "id": 8, 135 | "text": "A" 136 | }, 137 | { 138 | "type": "TOGGLE_TODO", 139 | "id": 7 140 | }, 141 | { 142 | "type": "TOGGLE_TODO", 143 | "id": 6 144 | }, 145 | { 146 | "type": "TOGGLE_TODO", 147 | "id": 5 148 | }, 149 | { 150 | "type": "FILTER_INCOMPLETE" 151 | }, 152 | { 153 | "type": "ADD_TODO", 154 | "id": 7, 155 | "text": "Let the future be rewritten" 156 | }, 157 | { 158 | "type": "DRAFT_TODO", 159 | "id": 7, 160 | "text": "Let the future be rewritten" 161 | }, 162 | { 163 | "type": "DRAFT_TODO", 164 | "id": 7, 165 | "text": "Let the future be rewritten" 166 | }, 167 | { 168 | "type": "DRAFT_TODO", 169 | "id": 7, 170 | "text": "Let the future be rew" 171 | }, 172 | { 173 | "type": "DRAFT_TODO", 174 | "id": 7, 175 | "text": "Let the future be re" 176 | }, 177 | { 178 | "type": "DRAFT_TODO", 179 | "id": 7, 180 | "text": "Let the future be r" 181 | }, 182 | { 183 | "type": "DRAFT_TODO", 184 | "id": 7, 185 | "text": "Let the future be " 186 | }, 187 | { 188 | "type": "DRAFT_TODO", 189 | "id": 7, 190 | "text": "Let the future " 191 | }, 192 | { 193 | "type": "DRAFT_TODO", 194 | "id": 7, 195 | "text": "Let the fut" 196 | }, 197 | { 198 | "type": "DRAFT_TODO", 199 | "id": 7, 200 | "text": "Let the f" 201 | }, 202 | { 203 | "type": "DRAFT_TODO", 204 | "id": 7, 205 | "text": "Let the " 206 | }, 207 | { 208 | "type": "DRAFT_TODO", 209 | "id": 7, 210 | "text": "Let the" 211 | }, 212 | { 213 | "type": "DRAFT_TODO", 214 | "id": 7, 215 | "text": "Let " 216 | }, 217 | { 218 | "type": "TOGGLE_TODO", 219 | "id": 4 220 | }, 221 | { 222 | "type": "ADD_TODO", 223 | "id": 6, 224 | "text": "Add scrubbers to walk through time" 225 | }, 226 | { 227 | "type": "DRAFT_TODO", 228 | "id": 6, 229 | "text": "Add scrubbers to walk through time" 230 | }, 231 | { 232 | "type": "DRAFT_TODO", 233 | "id": 6, 234 | "text": "Add scrubbers to walk through " 235 | }, 236 | { 237 | "type": "DRAFT_TODO", 238 | "id": 6, 239 | "text": "Add scrubbers to walk " 240 | }, 241 | { 242 | "type": "DRAFT_TODO", 243 | "id": 6, 244 | "text": "Add scrubbers to walk" 245 | }, 246 | { 247 | "type": "DRAFT_TODO", 248 | "id": 6, 249 | "text": "Add scrubbers to " 250 | }, 251 | { 252 | "type": "DRAFT_TODO", 253 | "id": 6, 254 | "text": "Add scrubbers " 255 | }, 256 | { 257 | "type": "DRAFT_TODO", 258 | "id": 6, 259 | "text": "Add scrubbers" 260 | }, 261 | { 262 | "type": "DRAFT_TODO", 263 | "id": 6, 264 | "text": "Add scru" 265 | }, 266 | { 267 | "type": "DRAFT_TODO", 268 | "id": 6, 269 | "text": "Add sc" 270 | }, 271 | { 272 | "type": "DRAFT_TODO", 273 | "id": 6, 274 | "text": "Add " 275 | }, 276 | { 277 | "type": "DRAFT_TODO", 278 | "id": 6, 279 | "text": "A" 280 | }, 281 | { 282 | "type": "TOGGLE_TODO", 283 | "id": 3 284 | }, 285 | { 286 | "type": "ADD_TODO", 287 | "id": 5, 288 | "text": "Time travel with cmd+z" 289 | }, 290 | { 291 | "type": "DRAFT_TODO", 292 | "id": 5, 293 | "text": "Time travel with cmd+z" 294 | }, 295 | { 296 | "type": "DRAFT_TODO", 297 | "id": 5, 298 | "text": "Time travel with cmd+z" 299 | }, 300 | { 301 | "type": "DRAFT_TODO", 302 | "id": 5, 303 | "text": "Time travel with cmd+" 304 | }, 305 | { 306 | "type": "DRAFT_TODO", 307 | "id": 5, 308 | "text": "Time travel with cmd" 309 | }, 310 | { 311 | "type": "DRAFT_TODO", 312 | "id": 5, 313 | "text": "Time travel with " 314 | }, 315 | { 316 | "type": "DRAFT_TODO", 317 | "id": 5, 318 | "text": "Time travel " 319 | }, 320 | { 321 | "type": "DRAFT_TODO", 322 | "id": 5, 323 | "text": "Time travel" 324 | }, 325 | { 326 | "type": "DRAFT_TODO", 327 | "id": 5, 328 | "text": "Time tra" 329 | }, 330 | { 331 | "type": "DRAFT_TODO", 332 | "id": 5, 333 | "text": "Time tr" 334 | }, 335 | { 336 | "type": "DRAFT_TODO", 337 | "id": 5, 338 | "text": "Time " 339 | }, 340 | { 341 | "type": "DRAFT_TODO", 342 | "id": 5, 343 | "text": "T" 344 | }, 345 | { 346 | "type": "ADD_TODO", 347 | "id": 4, 348 | "text": "Record history of user actions" 349 | }, 350 | { 351 | "type": "DRAFT_TODO", 352 | "id": 4, 353 | "text": "Record history of user actions" 354 | }, 355 | { 356 | "type": "DRAFT_TODO", 357 | "id": 4, 358 | "text": "Record history of user actions" 359 | }, 360 | { 361 | "type": "DRAFT_TODO", 362 | "id": 4, 363 | "text": "Record history of user ac" 364 | }, 365 | { 366 | "type": "DRAFT_TODO", 367 | "id": 4, 368 | "text": "Record history of user " 369 | }, 370 | { 371 | "type": "DRAFT_TODO", 372 | "id": 4, 373 | "text": "Record history of user" 374 | }, 375 | { 376 | "type": "DRAFT_TODO", 377 | "id": 4, 378 | "text": "Record history of us" 379 | }, 380 | { 381 | "type": "DRAFT_TODO", 382 | "id": 4, 383 | "text": "Record history of " 384 | }, 385 | { 386 | "type": "DRAFT_TODO", 387 | "id": 4, 388 | "text": "Record history " 389 | }, 390 | { 391 | "type": "DRAFT_TODO", 392 | "id": 4, 393 | "text": "Record histor" 394 | }, 395 | { 396 | "type": "DRAFT_TODO", 397 | "id": 4, 398 | "text": "Record hist" 399 | }, 400 | { 401 | "type": "DRAFT_TODO", 402 | "id": 4, 403 | "text": "Record " 404 | }, 405 | { 406 | "type": "DRAFT_TODO", 407 | "id": 4, 408 | "text": "Rec" 409 | }, 410 | { 411 | "type": "DRAFT_TODO", 412 | "id": 4, 413 | "text": "Re" 414 | }, 415 | { 416 | "type": "DRAFT_TODO", 417 | "id": 4, 418 | "text": "R" 419 | }, 420 | { 421 | "type": "ADD_TODO", 422 | "id": 3, 423 | "text": "Send input events through actions" 424 | }, 425 | { 426 | "type": "DRAFT_TODO", 427 | "id": 3, 428 | "text": "Send input events through actions" 429 | }, 430 | { 431 | "type": "DRAFT_TODO", 432 | "id": 3, 433 | "text": "Send input events through actions" 434 | }, 435 | { 436 | "type": "DRAFT_TODO", 437 | "id": 3, 438 | "text": "Send input events through action" 439 | }, 440 | { 441 | "type": "DRAFT_TODO", 442 | "id": 3, 443 | "text": "Send input events through ac" 444 | }, 445 | { 446 | "type": "DRAFT_TODO", 447 | "id": 3, 448 | "text": "Send input events through " 449 | }, 450 | { 451 | "type": "DRAFT_TODO", 452 | "id": 3, 453 | "text": "Send input events throu" 454 | }, 455 | { 456 | "type": "DRAFT_TODO", 457 | "id": 3, 458 | "text": "Send input events t" 459 | }, 460 | { 461 | "type": "DRAFT_TODO", 462 | "id": 3, 463 | "text": "Send input events " 464 | }, 465 | { 466 | "type": "DRAFT_TODO", 467 | "id": 3, 468 | "text": "Send input event" 469 | }, 470 | { 471 | "type": "DRAFT_TODO", 472 | "id": 3, 473 | "text": "Send input " 474 | }, 475 | { 476 | "type": "DRAFT_TODO", 477 | "id": 3, 478 | "text": "Send in" 479 | }, 480 | { 481 | "type": "DRAFT_TODO", 482 | "id": 3, 483 | "text": "Send " 484 | }, 485 | { 486 | "type": "DRAFT_TODO", 487 | "id": 3, 488 | "text": "S" 489 | }, 490 | { 491 | "type": "DRAFT_TODO", 492 | "id": 3, 493 | "text": "" 494 | }, 495 | { 496 | "type": "DRAFT_TODO", 497 | "id": 3, 498 | "text": "P" 499 | }, 500 | { 501 | "type": "TOGGLE_TODO", 502 | "id": 2 503 | }, 504 | { 505 | "type": "ADD_TODO", 506 | "id": 2, 507 | "text": "Create a todo app using redux" 508 | }, 509 | { 510 | "type": "DRAFT_TODO", 511 | "id": 2, 512 | "text": "Create a todo app using redux" 513 | }, 514 | { 515 | "type": "DRAFT_TODO", 516 | "id": 2, 517 | "text": "Create a todo app using redux" 518 | }, 519 | { 520 | "type": "DRAFT_TODO", 521 | "id": 2, 522 | "text": "Create a todo app using re" 523 | }, 524 | { 525 | "type": "DRAFT_TODO", 526 | "id": 2, 527 | "text": "Create a todo app using " 528 | }, 529 | { 530 | "type": "DRAFT_TODO", 531 | "id": 2, 532 | "text": "Create a todo app " 533 | }, 534 | { 535 | "type": "DRAFT_TODO", 536 | "id": 2, 537 | "text": "Create a todo app" 538 | }, 539 | { 540 | "type": "DRAFT_TODO", 541 | "id": 2, 542 | "text": "Create a todo a" 543 | }, 544 | { 545 | "type": "DRAFT_TODO", 546 | "id": 2, 547 | "text": "Create a todo " 548 | }, 549 | { 550 | "type": "DRAFT_TODO", 551 | "id": 2, 552 | "text": "Create a todo" 553 | }, 554 | { 555 | "type": "DRAFT_TODO", 556 | "id": 2, 557 | "text": "Create a " 558 | }, 559 | { 560 | "type": "DRAFT_TODO", 561 | "id": 2, 562 | "text": "Create " 563 | }, 564 | { 565 | "type": "DRAFT_TODO", 566 | "id": 2, 567 | "text": "C" 568 | }, 569 | { 570 | "type": "TOGGLE_TODO", 571 | "id": 1 572 | }, 573 | { 574 | "type": "ADD_TODO", 575 | "id": 1, 576 | "text": "Read about redux" 577 | }, 578 | { 579 | "type": "DRAFT_TODO", 580 | "id": 1, 581 | "text": "Read about redux" 582 | }, 583 | { 584 | "type": "DRAFT_TODO", 585 | "id": 1, 586 | "text": "Read about redux" 587 | }, 588 | { 589 | "type": "DRAFT_TODO", 590 | "id": 1, 591 | "text": "Read about red" 592 | }, 593 | { 594 | "type": "DRAFT_TODO", 595 | "id": 1, 596 | "text": "Read about r" 597 | }, 598 | { 599 | "type": "DRAFT_TODO", 600 | "id": 1, 601 | "text": "Read about " 602 | }, 603 | { 604 | "type": "DRAFT_TODO", 605 | "id": 1, 606 | "text": "Read ab" 607 | }, 608 | { 609 | "type": "DRAFT_TODO", 610 | "id": 1, 611 | "text": "Read " 612 | }, 613 | { 614 | "type": "DRAFT_TODO", 615 | "id": 1, 616 | "text": "R" 617 | }, 618 | { 619 | "type": "TOGGLE_TODO", 620 | "id": 0 621 | }, 622 | { 623 | "type": "ADD_TODO", 624 | "id": 0, 625 | "text": "Watch Dan Abramovs talk on redux" 626 | }, 627 | { 628 | "type": "DRAFT_TODO", 629 | "id": 0, 630 | "text": "Watch Dan Abramovs talk on redux" 631 | }, 632 | { 633 | "type": "DRAFT_TODO", 634 | "id": 0, 635 | "text": "Watch Dan Abramovs talk on redux" 636 | }, 637 | { 638 | "type": "DRAFT_TODO", 639 | "id": 0, 640 | "text": "Watch Dan Abramovs talk on re" 641 | }, 642 | { 643 | "type": "DRAFT_TODO", 644 | "id": 0, 645 | "text": "Watch Dan Abramovs talk on " 646 | }, 647 | { 648 | "type": "DRAFT_TODO", 649 | "id": 0, 650 | "text": "Watch Dan Abramovs talk " 651 | }, 652 | { 653 | "type": "DRAFT_TODO", 654 | "id": 0, 655 | "text": "Watch Dan Abramovs ta" 656 | }, 657 | { 658 | "type": "DRAFT_TODO", 659 | "id": 0, 660 | "text": "Watch Dan Abramovs t" 661 | }, 662 | { 663 | "type": "DRAFT_TODO", 664 | "id": 0, 665 | "text": "Watch Dan Abramovs " 666 | }, 667 | { 668 | "type": "DRAFT_TODO", 669 | "id": 0, 670 | "text": "Watch Dan Abramovs" 671 | }, 672 | { 673 | "type": "DRAFT_TODO", 674 | "id": 0, 675 | "text": "Watch Dan Abramov" 676 | }, 677 | { 678 | "type": "DRAFT_TODO", 679 | "id": 0, 680 | "text": "Watch Dan Abra" 681 | }, 682 | { 683 | "type": "DRAFT_TODO", 684 | "id": 0, 685 | "text": "Watch Dan A" 686 | }, 687 | { 688 | "type": "DRAFT_TODO", 689 | "id": 0, 690 | "text": "Watch Dan " 691 | }, 692 | { 693 | "type": "DRAFT_TODO", 694 | "id": 0, 695 | "text": "Watch " 696 | }, 697 | { 698 | "type": "DRAFT_TODO", 699 | "id": 0, 700 | "text": "Watc" 701 | }, 702 | { 703 | "type": "DRAFT_TODO", 704 | "id": 0, 705 | "text": "Wat" 706 | }, 707 | { 708 | "type": "DRAFT_TODO", 709 | "id": 0, 710 | "text": "Wa" 711 | }, 712 | { 713 | "type": "DRAFT_TODO", 714 | "id": 0, 715 | "text": "W" 716 | }, 717 | { 718 | "type": "BEGINNING_OF_TIME" 719 | } 720 | ] 721 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | todo with redux 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | Fork me on GitHub 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-redux", 3 | "version": "1.0.0", 4 | "description": "A project using redux, learning by doing", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run build && node server", 8 | "build": "./node_modules/.bin/webpack --bail", 9 | "develop": "HOT=true node server", 10 | "deploy": "webpack && git checkout -b build && git add static && git status && git commit -m 'Heroku build' && git checkout master && git push heroku build:master --force; git branch -D build", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/evanrs/use-redux.git" 16 | }, 17 | "author": "Evan Schneider", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/evanrs/use-redux/issues" 21 | }, 22 | "homepage": "https://github.com/evanrs/use-redux#readme", 23 | "dependencies": { 24 | "immutable": "^3.8.1", 25 | "lodash": "^4.17.4", 26 | "mousetrap": "^1.6.0", 27 | "react": "^15.4.2", 28 | "react-addons-css-transition-group": "^15.4.2", 29 | "react-dom": "^15.4.2", 30 | "react-hot-loader": "^3.0.0-beta.6", 31 | "react-redux": "^5.0.2", 32 | "react-tap-event-plugin": "^2.0.1", 33 | "redux": "^3.6.0", 34 | "redux-thunk": "^2.2.0" 35 | }, 36 | "devDependencies": { 37 | "autoprefixer": "^6.6.1", 38 | "babel-core": "^6.21.0", 39 | "babel-eslint": "^7.1.1", 40 | "babel-loader": "^6.2.10", 41 | "babel-plugin-react-transform": "^2.0.2", 42 | "babel-preset-es2015": "^6.18.0", 43 | "babel-preset-react": "^6.16.0", 44 | "babel-preset-stage-0": "^6.16.0", 45 | "css-loader": "^0.26.1", 46 | "eslint": "^3.13.1", 47 | "eslint-plugin-react": "^6.9.0", 48 | "express": "^4.14.0", 49 | "less": "^2.7.2", 50 | "less-loader": "^2.2.3", 51 | "postcss-loader": "^1.2.2", 52 | "redux-devtools": "^3.3.2", 53 | "style-loader": "^0.13.1", 54 | "webpack": "^2.2.0", 55 | "webpack-dev-middleware": "^1.9.0", 56 | "webpack-hot-middleware": "^2.15.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /reducers/filter.js: -------------------------------------------------------------------------------- 1 | import actions from '../actions'; 2 | 3 | const { FILTER_NONE, FILTER_COMPLETE, FILTER_INCOMPLETE } = actions.filter; 4 | 5 | const initialState = {type: FILTER_NONE, test: {}}; 6 | 7 | function filter(state=initialState, {type}) { 8 | switch (type) { 9 | case state.type: 10 | type = FILTER_NONE; 11 | case FILTER_NONE: 12 | return {type, test: {}}; 13 | 14 | case FILTER_COMPLETE: 15 | return {type, test: {complete: true}}; 16 | 17 | case FILTER_INCOMPLETE: 18 | return {type, test: {complete: false}}; 19 | 20 | default: 21 | return state; 22 | } 23 | } 24 | 25 | export default filter; 26 | -------------------------------------------------------------------------------- /reducers/index.js: -------------------------------------------------------------------------------- 1 | import Immutable, {Map} from 'immutable'; 2 | import forEach from 'lodash/forEach'; 3 | import mapValues from 'lodash/mapValues'; 4 | 5 | import filter from './filter'; 6 | import todos from './todos'; 7 | 8 | const reducers = { 9 | filter, 10 | todos 11 | } 12 | 13 | function mutableCombinator(state = {}, action) { 14 | return { 15 | ...state, 16 | ...mapValues(reducers, (reducer, key) => reducer(state[key], action)) 17 | } 18 | } 19 | 20 | function immutableCombinator(state = Map(), action) { 21 | if (! Map.isMap(state)) { 22 | state = Map(state); 23 | } 24 | 25 | return ( 26 | state.withMutations((state) => 27 | forEach( 28 | reducers, (reducer, key) => 29 | state.set( 30 | key, reducer(state.get(key), action, state)))) 31 | // Because redux-devtools can only take a plain object :-( 32 | .toObject() 33 | ) 34 | } 35 | 36 | export default immutableCombinator 37 | -------------------------------------------------------------------------------- /reducers/todos.js: -------------------------------------------------------------------------------- 1 | import Immutable, {Iterable, Map, List, Record} from 'immutable'; 2 | import isNumber from 'lodash/isNumber'; 3 | import matches from 'lodash/matches'; 4 | import uniqueId from 'lodash/uniqueId'; 5 | 6 | import actions from '../actions'; 7 | 8 | 9 | const { 10 | ADD_TODO, EDIT_TODO, SAVE_TODO, DRAFT_TODO, TOGGLE_TODO, REMOVE_TODO 11 | } = actions.todos; 12 | 13 | const ACTION_LIST = List([ 14 | ADD_TODO, EDIT_TODO, SAVE_TODO, DRAFT_TODO, TOGGLE_TODO, REMOVE_TODO]); 15 | 16 | const TodoRecord = Record( 17 | { id: 0, 18 | text: '', 19 | complete: false, 20 | drafting: true, 21 | editing: false}, 22 | 'TodoRecord' 23 | ); 24 | 25 | const initialState = Map({ 26 | count: 0, 27 | draft: new TodoRecord, 28 | items: List() 29 | }); 30 | 31 | function todos(state, {type, id, text} = {}) { 32 | state = 33 | ! Iterable.isKeyed(state) ? initialState 34 | : Map.isMap(state) ? state 35 | : Immutable.fromJS(state). 36 | update('list', list => list.map(v => new TodoRecord(v))). 37 | update('draft', v => new TodoRecord(v)); 38 | 39 | if (! ACTION_LIST.includes(type)) return state; 40 | 41 | let index = state.get('items').findIndex(matches({id})); 42 | 43 | switch(type) { 44 | case DRAFT_TODO: 45 | return state. 46 | update('draft', draft => 47 | draft.merge({text})) 48 | 49 | case ADD_TODO: { 50 | let count = state.get('count') + 1; 51 | 52 | return state. 53 | update('items', items => 54 | items.push(state.get('draft').merge({ 55 | drafting: false, 56 | text: text 57 | })) 58 | ). 59 | set('count', count). 60 | set('draft', new TodoRecord({id: count})); 61 | } 62 | 63 | case EDIT_TODO: 64 | return state. 65 | update('items', items => 66 | items.update(index, todo => 67 | todo.set('editing', true))); 68 | 69 | case SAVE_TODO: 70 | return state. 71 | update('items', items => 72 | items.update(index, todo => 73 | todo.set('editing', false).set('text', text))); 74 | 75 | case TOGGLE_TODO: 76 | if (index >= 0) 77 | return state. 78 | update('items', items => 79 | items.update(index, todo => 80 | todo.set('complete', ! todo.complete))); 81 | 82 | case REMOVE_TODO: 83 | if (index >= 0) 84 | return state. 85 | update('items', items => 86 | items.delete(index)); 87 | 88 | default: 89 | console.error("Invalid state, undefined index", type, id, text); 90 | } 91 | 92 | return state; 93 | } 94 | 95 | export default todos; 96 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const express = require('express'); 4 | const devMiddleware = require('webpack-dev-middleware'); 5 | const hotMiddleware = require('webpack-hot-middleware'); 6 | 7 | const config = require('./webpack.config'); 8 | 9 | const PORT = process.env.PORT || 3000; 10 | const HOT = process.env.HOT; 11 | 12 | const app = express(); 13 | 14 | if (HOT) { 15 | const compiler = webpack(config); 16 | app.use(devMiddleware(compiler, { 17 | // noInfo: true, 18 | publicPath: config.output.publicPath, 19 | historyApiFallback: true 20 | })); 21 | 22 | app.use(hotMiddleware(compiler)); 23 | } 24 | 25 | app.use('/static', express.static('static')); 26 | 27 | app.get('*', function (req, res) { 28 | res.sendFile(path.join(__dirname, 'index.html')); 29 | }); 30 | 31 | app.listen(PORT, function (err) { 32 | if (err) { 33 | console.log(err); 34 | return; 35 | } 36 | 37 | console.log('Listening at', PORT); 38 | }); 39 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanrs/use-redux/b774662adb54e0be60ed7447652671b51016a301/static/.gitkeep -------------------------------------------------------------------------------- /store/action-history.js: -------------------------------------------------------------------------------- 1 | import Mousetrap from 'mousetrap'; 2 | import 'mousetrap/plugins/global-bind/mousetrap-global-bind'; 3 | 4 | const identity = (x) => x; 5 | 6 | export function createHistory(volatile = identity, namespace='_actionHistory') { 7 | return (next) => (reducer, initialState) => { 8 | initialState = reducer(initialState, {cursor: 0}); 9 | 10 | let history = JSON.parse(localStorage.getItem(namespace) || '[]'); 11 | let futureState = history.reduceRight(reducer, initialState) || {}; 12 | 13 | function historyReducer (state, action) { 14 | let cursor = state.cursor || 0; 15 | let historySize = history.length; 16 | 17 | switch (action.type) { 18 | case '@@GOTO': 19 | cursor = action.cursor; 20 | return history. 21 | slice(cursor). 22 | reduceRight(reducer, {...initialState, cursor, historySize}) 23 | 24 | case '@@UNDO': 25 | cursor = cursor < history.length ? cursor + 1 : cursor; 26 | return history. 27 | slice(cursor). 28 | reduceRight(reducer, {...initialState, cursor, historySize}) 29 | 30 | case '@@REDO': 31 | cursor = cursor > 0 ? cursor - 1 : 0; 32 | return history. 33 | slice(cursor). 34 | reduceRight(reducer, {...initialState, cursor, historySize}) 35 | 36 | default: 37 | return [action, ...history.slice(cursor)]. 38 | reduceRight(reducer, {...initialState, cursor, historySize}); 39 | } 40 | } 41 | 42 | let store = next(historyReducer, futureState); 43 | 44 | let dispatch = (action) => { 45 | store.dispatch(action); 46 | 47 | let cursor = store.getState().cursor || 0; 48 | 49 | if (! /@@(UNDO|REDO|GOTO)$/.test(action.type)) { 50 | let future = history.slice(0, cursor). 51 | filter(future => ! volatile(action, future)); 52 | 53 | history = [ 54 | ...future, action, ...history.slice(cursor)]; 55 | } 56 | 57 | localStorage.setItem( 58 | namespace, JSON.stringify(history.slice(cursor, 1000))); 59 | 60 | return action; 61 | } 62 | 63 | Mousetrap.bindGlobal('command+z', event => { 64 | event.preventDefault(); 65 | dispatch({type: '@@UNDO'}) 66 | }); 67 | 68 | Mousetrap.bindGlobal('shift+command+z', event => { 69 | event.preventDefault(); 70 | dispatch({type: '@@REDO'}) 71 | }); 72 | 73 | return {...store, dispatch} 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /store/history.js: -------------------------------------------------------------------------------- 1 | import Mousetrap from 'mousetrap'; 2 | import 'mousetrap/plugins/global-bind/mousetrap-global-bind'; 3 | 4 | export function createHistory(namespace='_history') { 5 | return (next) => (reducer, initialState) => { 6 | let history = JSON.parse(localStorage.getItem(namespace) || '[]'); 7 | let [previousState] = history; 8 | 9 | function historyReducer (state, action) { 10 | let cursor = state.cursor || 0; 11 | 12 | switch (action.type) { 13 | case '@@UNDO': 14 | cursor = cursor < history.length ? cursor + 1 : cursor; 15 | return reducer({...history[cursor], cursor}, action); 16 | 17 | case '@@REDO': 18 | cursor = cursor > 0 ? cursor - 1 : 0; 19 | return reducer({...history[cursor], cursor}, action); 20 | 21 | default: 22 | history = history.slice(cursor); 23 | cursor = 0; 24 | return reducer({...state, cursor}, action); 25 | } 26 | } 27 | 28 | let store = next(historyReducer, {...initialState, ...previousState}); 29 | let dispatch = (action) => { 30 | action = store.dispatch(action); 31 | 32 | let state = store.getState(); 33 | 34 | if (! /@@(UN|RE)DO$/.test(action.type)) 35 | history = [state, ...history]; 36 | 37 | localStorage.setItem( 38 | namespace, 39 | JSON.stringify( 40 | history.slice(state.cursor, 100))); 41 | 42 | return action; 43 | } 44 | 45 | Mousetrap.bindGlobal('command+z', event => { 46 | event.preventDefault(); 47 | dispatch({type: '@@UNDO'}) 48 | }); 49 | 50 | Mousetrap.bindGlobal('shift+command+z', event => { 51 | event.preventDefault(); 52 | dispatch({type: '@@REDO'}) 53 | }); 54 | 55 | return {...store, dispatch} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import { devTools, persistState } from 'redux-devtools'; 5 | import thunk from 'redux-thunk'; 6 | 7 | import { createHistory } from './action-history'; 8 | // import { createHistory } from './history'; 9 | import reducers from '../reducers'; 10 | 11 | const createCustomStore = 12 | compose(...[ 13 | applyMiddleware(thunk), 14 | createHistory((current, future) => 15 | // Toogle creates unexpected behavior, investigate filter 16 | // /TOGGLE/g.test(current.type) && /TOGGLE/g.test(future.type) && current.id === future.id || 17 | current.type === future.type && /FILTER/g.test(current.type) && /FILTER/g.test(future.type) || 18 | current.id === future.id && /DRAFT/g.test(current.type) && /REMOVE/g.test(future.type) || 19 | current.id === future.id && /REMOVE|ADD/g.test(current.type) && /ADD|DRAFT/g.test(future.type) || 20 | /REMOVE|ADD/g.test(current.type) && /TOGGLE/g.test(future.type) 21 | ), 22 | // module.hot && devTools(), 23 | // module.hot && persistState( 24 | // window.location.href.match(/[?&]debug_session=([^&]+)\b/)) 25 | ].filter(x => x) 26 | )(createStore) 27 | ; 28 | 29 | export default function configure() { 30 | const store = createCustomStore(reducers); 31 | 32 | if (module.hot) { 33 | // Enable Webpack hot module replacement for reducers 34 | module.hot.accept('../reducers', () => { 35 | const nextReducer = require('../reducers'); 36 | store.replaceReducer(nextReducer); 37 | }); 38 | } 39 | 40 | return store; 41 | } 42 | -------------------------------------------------------------------------------- /style/blue-theme.less: -------------------------------------------------------------------------------- 1 | @import './styleguide/core.less'; 2 | @import './styleguide/themes/default.less'; 3 | @import './style.less'; 4 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | if (/green/.test(location.href)) { 2 | require('./mint-theme.less'); 3 | } 4 | else if (/red/.test(location.href)) { 5 | require('./red-theme.less'); 6 | } 7 | else { 8 | require('./blue-theme.less') 9 | } 10 | -------------------------------------------------------------------------------- /style/mint-theme.less: -------------------------------------------------------------------------------- 1 | @import './styleguide/core.less'; 2 | @import './styleguide/themes/friends-and-foes.less'; 3 | @import './style.less'; 4 | -------------------------------------------------------------------------------- /style/range.less: -------------------------------------------------------------------------------- 1 | .history-scrubber-appear { 2 | opacity: 0; 3 | transform: 4 | translateY(18px) 5 | rotateX(-10deg); 6 | } 7 | .history-scrubber-appear.history-scrubber-appear-active { 8 | opacity: 1; 9 | transform: scaleX(1) scaleY(1) rotateX(0deg) translateY(0%); 10 | transition: opacity .33s ease-in, transform 0.35s ease-out; 11 | } 12 | 13 | .history-scrubber { 14 | max-width: 320px; 15 | width: 100%; 16 | margin: 0 auto; 17 | padding: 10px; 18 | position: fixed; 19 | left: 50%; 20 | bottom: 0; 21 | transform: translateX(-50%); 22 | background-image: linear-gradient(to bottom, fade(@body-background, 0) 0%, @body-background 95%); 23 | z-index: 4; 24 | 25 | h6 { 26 | font-weight: 600; 27 | text-transform: lowercase; 28 | font-variant: small-caps; 29 | text-shadow: 0px 0px 4px @body-background; 30 | } 31 | } 32 | 33 | input[type=range] { 34 | -webkit-appearance: none; 35 | width: 100%; 36 | margin: 10.6px 0; 37 | } 38 | input[type=range]:focus { 39 | outline: none; 40 | } 41 | input[type=range]::-webkit-slider-runnable-track { 42 | width: 100%; 43 | height: 15px; 44 | cursor: pointer; 45 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 46 | background: #f0f0f0; 47 | border-radius: 3.8px; 48 | border: 3px solid #ffffff; 49 | } 50 | input[type=range]::-webkit-slider-thumb { 51 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 52 | border: 3px solid #e6e6e6; 53 | height: 36px; 54 | width: 16px; 55 | border-radius: 16px; 56 | background: #ffffff; 57 | cursor: pointer; 58 | -webkit-appearance: none; 59 | margin-top: -13.6px; 60 | } 61 | input[type=range]:focus::-webkit-slider-runnable-track { 62 | background: #f8f8f8; 63 | } 64 | input[type=range]::-moz-range-track { 65 | width: 100%; 66 | height: 15px; 67 | cursor: pointer; 68 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 69 | background: #f0f0f0; 70 | border-radius: 3.8px; 71 | border: 3px solid #ffffff; 72 | } 73 | input[type=range]::-moz-range-thumb { 74 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 75 | border: 3px solid #e6e6e6; 76 | height: 36px; 77 | width: 16px; 78 | border-radius: 16px; 79 | background: #ffffff; 80 | cursor: pointer; 81 | } 82 | input[type=range]::-ms-track { 83 | width: 100%; 84 | height: 15px; 85 | cursor: pointer; 86 | background: transparent; 87 | border-color: transparent; 88 | color: transparent; 89 | } 90 | input[type=range]::-ms-fill-lower { 91 | background: #e8e8e8; 92 | border: 3px solid #ffffff; 93 | border-radius: 7.6px; 94 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 95 | } 96 | input[type=range]::-ms-fill-upper { 97 | background: #f0f0f0; 98 | border: 3px solid #ffffff; 99 | border-radius: 7.6px; 100 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 101 | } 102 | input[type=range]::-ms-thumb { 103 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 104 | border: 3px solid #e6e6e6; 105 | height: 36px; 106 | width: 16px; 107 | border-radius: 16px; 108 | background: #ffffff; 109 | cursor: pointer; 110 | height: 15px; 111 | } 112 | input[type=range]:focus::-ms-fill-lower { 113 | background: #f0f0f0; 114 | } 115 | input[type=range]:focus::-ms-fill-upper { 116 | background: #f8f8f8; 117 | } 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | input[type=range] { 127 | -webkit-appearance: none; 128 | width: 100%; 129 | margin: 10.6px 0; 130 | } 131 | input[type=range]:focus { 132 | outline: none; 133 | } 134 | input[type=range]::-webkit-slider-runnable-track { 135 | width: 100%; 136 | height: 15px; 137 | cursor: pointer; 138 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 139 | background: #f0f0f0; 140 | border-radius: 3.8px; 141 | border: 3px solid #ffffff; 142 | } 143 | input[type=range]::-webkit-slider-thumb { 144 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 145 | border: 3px solid #e6e6e6; 146 | height: 36px; 147 | width: 16px; 148 | border-radius: 16px; 149 | background: #ffffff; 150 | cursor: pointer; 151 | -webkit-appearance: none; 152 | margin-top: -13.6px; 153 | } 154 | input[type=range]:focus::-webkit-slider-runnable-track { 155 | background: #f8f8f8; 156 | } 157 | input[type=range]::-moz-range-track { 158 | width: 100%; 159 | height: 15px; 160 | cursor: pointer; 161 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 162 | background: #f0f0f0; 163 | border-radius: 3.8px; 164 | border: 3px solid #ffffff; 165 | } 166 | input[type=range]::-moz-range-thumb { 167 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 168 | border: 3px solid #e6e6e6; 169 | height: 36px; 170 | width: 16px; 171 | border-radius: 16px; 172 | background: #ffffff; 173 | cursor: pointer; 174 | } 175 | input[type=range]::-ms-track { 176 | width: 100%; 177 | height: 15px; 178 | cursor: pointer; 179 | background: transparent; 180 | border-color: transparent; 181 | color: transparent; 182 | } 183 | input[type=range]::-ms-fill-lower { 184 | background: #e8e8e8; 185 | border: 3px solid #ffffff; 186 | border-radius: 7.6px; 187 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 188 | } 189 | input[type=range]::-ms-fill-upper { 190 | background: #f0f0f0; 191 | border: 3px solid #ffffff; 192 | border-radius: 7.6px; 193 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 194 | } 195 | input[type=range]::-ms-thumb { 196 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 197 | border: 3px solid #e6e6e6; 198 | height: 36px; 199 | width: 16px; 200 | border-radius: 16px; 201 | background: #ffffff; 202 | cursor: pointer; 203 | height: 15px; 204 | } 205 | input[type=range]:focus::-ms-fill-lower { 206 | background: #f0f0f0; 207 | } 208 | input[type=range]:focus::-ms-fill-upper { 209 | background: #f8f8f8; 210 | } 211 | 212 | 213 | 214 | 215 | 216 | 217 | input[type=range] { 218 | -webkit-appearance: none; 219 | width: 100%; 220 | margin: 10.6px 0; 221 | } 222 | input[type=range]:focus { 223 | outline: none; 224 | } 225 | input[type=range]::-webkit-slider-runnable-track { 226 | width: 100%; 227 | height: 15px; 228 | cursor: pointer; 229 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 230 | background: #f0f0f0; 231 | border-radius: 3.8px; 232 | border: 3px solid #ffffff; 233 | } 234 | input[type=range]::-webkit-slider-thumb { 235 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 236 | border: 3px solid #e6e6e6; 237 | height: 36px; 238 | width: 16px; 239 | border-radius: 16px; 240 | background: #ffffff; 241 | cursor: pointer; 242 | -webkit-appearance: none; 243 | margin-top: -13.6px; 244 | } 245 | input[type=range]:focus::-webkit-slider-runnable-track { 246 | background: #f8f8f8; 247 | } 248 | input[type=range]::-moz-range-track { 249 | width: 100%; 250 | height: 15px; 251 | cursor: pointer; 252 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 253 | background: #f0f0f0; 254 | border-radius: 3.8px; 255 | border: 3px solid #ffffff; 256 | } 257 | input[type=range]::-moz-range-thumb { 258 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 259 | border: 3px solid #e6e6e6; 260 | height: 36px; 261 | width: 16px; 262 | border-radius: 16px; 263 | background: #ffffff; 264 | cursor: pointer; 265 | } 266 | input[type=range]::-ms-track { 267 | width: 100%; 268 | height: 15px; 269 | cursor: pointer; 270 | background: transparent; 271 | border-color: transparent; 272 | color: transparent; 273 | } 274 | input[type=range]::-ms-fill-lower { 275 | background: #e8e8e8; 276 | border: 3px solid #ffffff; 277 | border-radius: 7.6px; 278 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 279 | } 280 | input[type=range]::-ms-fill-upper { 281 | background: #f0f0f0; 282 | border: 3px solid #ffffff; 283 | border-radius: 7.6px; 284 | box-shadow: 1px 1px 1px #919193, 0px 0px 1px #9e9ea0; 285 | } 286 | input[type=range]::-ms-thumb { 287 | box-shadow: 0px 0px 3px 0px #666, 0px 0px 0px #ffffff; 288 | border: 3px solid #e6e6e6; 289 | height: 36px; 290 | width: 16px; 291 | border-radius: 16px; 292 | background: #ffffff; 293 | cursor: pointer; 294 | height: 15px; 295 | } 296 | input[type=range]:focus::-ms-fill-lower { 297 | background: #f0f0f0; 298 | } 299 | input[type=range]:focus::-ms-fill-upper { 300 | background: #f8f8f8; 301 | } 302 | 303 | -------------------------------------------------------------------------------- /style/red-theme.less: -------------------------------------------------------------------------------- 1 | @import './styleguide/core.less'; 2 | @import './styleguide/themes/ocean-sunset.less'; 3 | @import './style.less'; 4 | -------------------------------------------------------------------------------- /style/style.less: -------------------------------------------------------------------------------- 1 | @import './styleguide/core.less'; 2 | @import './range.less'; 3 | 4 | // @import './styleguide/themes/friends-and-foes.less'; 5 | // @import './styleguide/themes/ocean-sunset.less'; 6 | // @import './styleguide/themes/phaedra.less'; 7 | 8 | 9 | @size-icon: 30px; 10 | 11 | .ui-text { 12 | font-size: 12px; 13 | margin: 0 0.5em; 14 | } 15 | 16 | a, .a { 17 | font-size: 12px; 18 | margin: 0 0.5em; 19 | 20 | &.selected { 21 | text-decoration: underline; 22 | } 23 | } 24 | 25 | .a { 26 | user-select: none; 27 | } 28 | 29 | .scrub { 30 | font-family: Avenir, Helvetica, sans-serif; 31 | padding: 15px 30px; 32 | } 33 | 34 | .todoitem { 35 | position: relative; 36 | display: flex; 37 | width: 100%; 38 | align-items: center; 39 | font-size: 16px; 40 | font-weight: 100; 41 | } 42 | .todoitem-text { 43 | flex: 1; 44 | padding: 16px; 45 | font-size: 13px; 46 | opacity: 1; 47 | transition: opacity 0.1s; 48 | &.todoitem-text__complete { 49 | opacity: 0.5; 50 | } 51 | } 52 | .todoitem-delete { 53 | padding: 10px 5px; 54 | font-size: 24px; 55 | line-height: 24px; 56 | 57 | font-family: Raleway; 58 | font-weight: 500; 59 | color: #FFF; 60 | background: none; 61 | cursor: pointer; 62 | user-select: none; 63 | 64 | &, &:active { 65 | outline: none; 66 | } 67 | } 68 | 69 | .check { 70 | &, * { 71 | user-select: none; 72 | } 73 | 74 | width: @size-icon; 75 | height: @size-icon; 76 | background: @ui-background; 77 | border-radius: 50%; 78 | position: relative; 79 | box-shadow: 0px -4px 4px 0px rgba(255, 255, 255, 0), inset 0px 4px 4px 0px rgba(0, 0, 0, 0.08); 80 | cursor: pointer; 81 | 82 | &:before { 83 | content: ""; 84 | background: @ui-foreground; 85 | position: absolute; 86 | border-radius: 50%; 87 | top: 8%; 88 | left: 9%; 89 | right: 9%; 90 | bottom: 11%; 91 | box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.2); 92 | } 93 | 94 | &:after { 95 | content: ""; 96 | position: absolute; 97 | border-radius: 50%; 98 | top: 0; 99 | left: 0; 100 | right: 0; 101 | bottom: 0; 102 | border-color: @affirmation; 103 | border-width: 0; 104 | border-style: solid; 105 | } 106 | 107 | .check-icon { 108 | position: absolute; 109 | z-index: 3; 110 | top: -35%; 111 | left: 28%; 112 | width: 45%; 113 | fill: @ui-text; 114 | } 115 | 116 | 117 | &.active { 118 | &:after { 119 | transition: 0.1s ease-out all; 120 | border-width: (@size-icon / 2); 121 | } 122 | 123 | .check-icon { 124 | fill: @affirmation-text; 125 | animation: bounce 0.2s; 126 | animation-delay: 0.08s; 127 | } 128 | } 129 | 130 | &.inactive { 131 | &:after { 132 | transition: 0.1s ease-out all; 133 | } 134 | 135 | .check-icon { 136 | animation: unbounce 0.3s; 137 | animation-delay: 0.08s; 138 | } 139 | } 140 | 141 | } 142 | 143 | 144 | @keyframes unbounce { 145 | 0% { transform: scale(1.2); } 146 | 50% { transform: scale(0.8); } 147 | 100% { transform: scale(1); } 148 | } 149 | 150 | @keyframes bounce { 151 | 0% { transform: scale(1); } 152 | 50% { transform: scale(1.2); } 153 | 100% { transform: scale(1); } 154 | } 155 | 156 | .todoList-appear { 157 | opacity: 0; 158 | transform: scale(0.98) translateY(-20%); 159 | 160 | & ~ * { 161 | transform: translateY(-100%); 162 | } 163 | } 164 | .todoList-appear.todoList-appear-active { 165 | opacity: 1; 166 | transition: opacity .15s ease-in, transform .15s ease-out; 167 | transform: scale(1) translateY(0%); 168 | 169 | & ~ * { 170 | transition: transform .1s ease-in-out; 171 | transform: translateY(0%); 172 | } 173 | } 174 | 175 | .todoList-enter { 176 | opacity: 0; 177 | transform: scale(0.98) translateY(-20%); 178 | 179 | & ~ .todoList { 180 | transform: translateY(-100%); 181 | } 182 | } 183 | 184 | .todoList-enter.todoList-enter-active { 185 | opacity: 1; 186 | transition: opacity .15s ease-in, transform .15s ease-out; 187 | transform: scale(1) translateY(0%); 188 | 189 | & ~ * { 190 | transition: transform .1s ease-in-out; 191 | transform: translateY(0%); 192 | } 193 | } 194 | 195 | .todoList-leave { 196 | opacity: 1; 197 | transform: scale(1) translateY(0%); 198 | 199 | & ~ * { 200 | transform: translateY(0%); 201 | } 202 | } 203 | 204 | .todoList-leave.todoList-leave-active { 205 | opacity: 0; 206 | transition: opacity .15s ease-in, transform .15s ease-out; 207 | transform: scale(0.98) translateY(-20%); 208 | 209 | & ~ .todoList { 210 | transition: transform .125s ease-in-out; 211 | transform: translateY(-100%); 212 | } 213 | } 214 | 215 | .rule { 216 | padding: 16px; 217 | hr { 218 | margin: 0; 219 | border: none; 220 | max-height: 4px; 221 | height: 4px; 222 | border-radius: 3px; 223 | background: @ui-border; 224 | } 225 | } 226 | 227 | .app-appear { 228 | opacity: 0; 229 | transform: 230 | scaleY(1.15) 231 | scaleX(0.95) 232 | translateY(16px) 233 | rotateX(-40deg) 234 | ; 235 | } 236 | .app-appear.app-appear-active { 237 | opacity: 1; 238 | transform: scaleX(1) scaleY(1) rotateX(0deg) translateY(0%); 239 | transition: opacity .33s ease-in, transform 0.35s ease-out; 240 | } 241 | -------------------------------------------------------------------------------- /style/styleguide/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /style/styleguide/colors.less: -------------------------------------------------------------------------------- 1 | @blue125: #5288F7; 2 | @blue: #4d88ff; 3 | @blue75: #78A4FF; 4 | @blue50: #CDDDFF; 5 | @blue25: #E6EEFF; 6 | 7 | @black: #1E2835; 8 | @black75: #404853; 9 | @black50: #8E939A; 10 | @black25: #E0E0E0; 11 | @black15: #F6F7F7; 12 | @black5: #FAFBFB; 13 | @black0: #FFFFFF; 14 | 15 | @yellow: #F9D04B; 16 | @yellow75: #FBDC79; 17 | @yellow50: #FCE8A5; 18 | @yellow25: #FEF4D2; 19 | 20 | @red: #EE4D4E; 21 | @red75: #F37A7B; 22 | @red50: #F7A6A7; 23 | @red25: #FBD3D3; 24 | 25 | @green: #64D481; 26 | @green75: #93E1A7; 27 | @green50: #C1EECD; 28 | @green25: #E0F7E6; 29 | 30 | @ocean: #65B5C6; 31 | @ocean20: mix(@ocean, @black0, 20%); 32 | @ocean10: mix(@ocean, @black0, 10%); 33 | 34 | @ruby: #E94F50; 35 | @ruby20: mix(@ruby, @black0, 20%); 36 | @ruby10: mix(@ruby, @black0, 10%); 37 | 38 | @mango: #FFDA63; 39 | @mango20: mix(@mango, @black0, 20%); 40 | @mango10: mix(@mango, @black0, 10%); 41 | 42 | @lime: #A0DA6B; 43 | @lime20: mix(@lime, @black0, 20%); 44 | @lime10: mix(@lime, @black0, 10%); 45 | 46 | @orange: #FF8C50; 47 | @orange20: mix(@orange, @black0, 20%); 48 | @orange10: mix(@orange, @black0, 10%); 49 | 50 | @purple: #A695ED; 51 | @purple20: mix(@purple, @black0, 20%); 52 | @purple10: mix(@purple, @black0, 10%); 53 | 54 | @pink: #FF75B6; 55 | @pink20: mix(@pink, @black0, 20%); 56 | @pink10: mix(@pink, @black0, 10%); 57 | 58 | @charcoal: #343D49; 59 | @charcoal20: mix(@charcoal, @black0, 20%); 60 | @charcoal10: mix(@charcoal, @black0, 10%); 61 | 62 | // too many colors 63 | @bluer125: #4073D6; 64 | @bluer110: #5262FF; 65 | @bluer: #4D8AFF; 66 | @salmon: #FF8766; 67 | @greenr125: #6FB24A; 68 | @greenr: #66D02C; 69 | -------------------------------------------------------------------------------- /style/styleguide/core.less: -------------------------------------------------------------------------------- 1 | @import './reset.less'; 2 | @import './media.less'; 3 | @import './colors.less'; 4 | @import './units.less'; 5 | 6 | @import './themes/default.less'; 7 | 8 | * { 9 | box-sizing: border-box 10 | } 11 | 12 | body { 13 | background-color: @body-background; 14 | color: @body-foreground; 15 | font-family: 'Muli', sans-serif; 16 | font-weight: 100; 17 | } 18 | 19 | h1, h2, h3, h4, h5, h6 { 20 | font-family: 'Raleway', sans-serif; 21 | } 22 | 23 | p { 24 | line-height: 1.5em; 25 | } 26 | 27 | a, .a { 28 | text-decoration: none; 29 | color: inherit; 30 | cursor: pointer; 31 | 32 | &:active, &:hover { 33 | color: @mango; 34 | } 35 | } 36 | 37 | .overlay { 38 | width: 100%; 39 | text-align: center; 40 | 41 | position: absolute; 42 | top: 50%; 43 | left: 50%; 44 | transform: translate(-50%, -50%); 45 | } 46 | 47 | .jumbotron { 48 | position: relative; 49 | text-align: center; 50 | min-height: 50%; 51 | margin: 10% 0; 52 | h1 { 53 | font-size: 35px; 54 | line-height: 60px; 55 | font-weight: 100; 56 | @media @nonmobile { 57 | font-size: 60px; 58 | margin-bottom: 15px; 59 | } 60 | } 61 | .subheading { 62 | position: absolute; 63 | right: 0; 64 | font-weight: 300; 65 | font-size: 12px; 66 | font-variant: small-caps; 67 | } 68 | } 69 | 70 | input, textarea { 71 | -moz-appearance: none; 72 | -webkit-appearance: none; 73 | appearance: none; 74 | display: block; 75 | max-width: 100%; 76 | border-radius: 3px; 77 | border: none; 78 | background-color: @input-background-color; 79 | background-image: @input-background-image; 80 | 81 | &:focus { 82 | background-color: @input-background-color-focus; 83 | } 84 | 85 | font-size: 16px; 86 | line-height: 24px; 87 | color: @black75; 88 | } 89 | 90 | input[type="text"] { 91 | padding: 0.5em 1em; 92 | margin-bottom: @base-size; 93 | 94 | &:focus { 95 | outline: none; 96 | box-shadow: 0 0 0px 3px @ui-border; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /style/styleguide/media.less: -------------------------------------------------------------------------------- 1 | /* handheld vertical-only resolution */ 2 | @tiny: ~"screen and (max-width: 479px)"; 3 | 4 | /* handheld horizontal + oddball devices */ 5 | @mobile: ~"screen and (max-width: 639px)"; 6 | 7 | /* non-handheld screens */ 8 | @nonmobile: ~"screen and (min-width: 640px)"; 9 | 10 | /* tablet vertical */ 11 | @tablet: ~"screen and (min-width: 768px)"; 12 | 13 | /* small laptop / ipad horizontal */ 14 | @big: ~"screen and (min-width: 1024px)"; 15 | 16 | /* laptop / desktop full-screen browser */ 17 | @bigger: ~"screen and (min-width: 1192px)"; 18 | 19 | /* 'venti' desktop full-screen browser */ 20 | @biggest: ~"screen and (min-width: 1280px)"; 21 | 22 | /* What are you, on a imperial star destroyer? */ 23 | @intergalactic: ~"screen and (min-width: 1380px)"; 24 | -------------------------------------------------------------------------------- /style/styleguide/reset.less: -------------------------------------------------------------------------------- 1 | /* Eric Meyer's Reset CSS v2.0 - http://cssreset.com */ 2 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0} 3 | -------------------------------------------------------------------------------- /style/styleguide/themes/default.less: -------------------------------------------------------------------------------- 1 | @import '../colors'; 2 | 3 | @body-background: @blue; 4 | @body-foreground: #fff; 5 | 6 | @ui-border: fade(@black0, 50); 7 | @ui-background: rgba(255,255,255, 0.85); 8 | @ui-foreground: #fff; 9 | @ui-text: @black25; 10 | 11 | @affirmation: mix(#60b247, @greenr); 12 | @affirmation-text: @black0; 13 | 14 | @input-background-color: mix(@black25, @black50, 25%); 15 | @input-background-color-focus: transparent; 16 | @input-background-image: linear-gradient(to bottom, @black0 0%, @black5 25%, @black15 45%, fade(@black15, 75) 95%); 17 | -------------------------------------------------------------------------------- /style/styleguide/themes/friends-and-foes.less: -------------------------------------------------------------------------------- 1 | @import '../colors'; 2 | 3 | @theme_black: #2F2933; 4 | 5 | @theme_ocean: @theme_ocean50; 6 | @theme_ocean100: #003E40; 7 | @theme_ocean70: #017C7F; 8 | @theme_ocean60: #01A2A6; 9 | @theme_ocean50: #01E0E5; 10 | @theme_ocean40: #02F9FF; 11 | 12 | 13 | @theme_mint: @theme_mint50; 14 | @theme_mint100: #0E4A42; 15 | @theme_mint70: #1EA190; 16 | @theme_mint60: #25C5B0; 17 | @theme_mint50: #29D9C2; 18 | @theme_mint40: #2FFAE0; 19 | 20 | @theme_lime: @theme_lime50; 21 | @theme_lime70: #91BA57; 22 | @theme_lime60: #ADDE67; 23 | @theme_lime50: #BDF271; 24 | 25 | @theme_yellow: #FFFFA6; 26 | 27 | 28 | @body-background: @theme_mint; 29 | @body-foreground: @black0; 30 | 31 | @affirmation: @mango; 32 | 33 | @ui-border: fade(@black0, 40); 34 | @ui-background: fade(@black0, 70); 35 | @ui-foreground: fade(@black0, 50); 36 | @ui-text: fade(@black50, 25); 37 | 38 | // @input-background-image: linear-gradient(to bottom, @black0 0%, @black5 25%, @black15 45%, fade(@black15, 75) 95%); 39 | -------------------------------------------------------------------------------- /style/styleguide/themes/ocean-sunset.less: -------------------------------------------------------------------------------- 1 | @taupe: #405952; 2 | @olive: #9C9B7A; 3 | @tan: #FFD393; 4 | @orange: #FF974F; 5 | @red: #F54F29; 6 | 7 | @body-background: @orange; 8 | @body-foreground: @tan; 9 | 10 | @affirmation: @black; 11 | 12 | @ui-border: fade(@tan, 40); 13 | @ui-background: fade(@red, 100); 14 | @ui-foreground: fade(@black0, 12); 15 | @ui-text: fade(@black0, 100); 16 | -------------------------------------------------------------------------------- /style/styleguide/themes/phaedra.less: -------------------------------------------------------------------------------- 1 | @red: #FF6138; 2 | @yellow: #FFFF9D; 3 | @green: #BEEB9F; 4 | @green50: #79BD8F; 5 | @green75: #00A388; 6 | 7 | @body-background: @green75; 8 | @body-foreground: @green; 9 | 10 | @affirmation: mix(@red, @mango); 11 | 12 | @ui-border: fade(@yellow, 40); 13 | @ui-background: fade(@green, 70); 14 | @ui-foreground: fade(@yellow, 50); 15 | @ui-text: fade(@green75, 25); 16 | -------------------------------------------------------------------------------- /style/styleguide/units.less: -------------------------------------------------------------------------------- 1 | @base-size: 16px; 2 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const filter = require('lodash/filter'); 4 | 5 | const HOT = process.env.HOT; 6 | 7 | module.exports = { 8 | devtool: HOT ? 'eval-source-map' : 'source-map', 9 | entry: filter([ 10 | HOT && 'react-hot-loader/patch', 11 | HOT && 'webpack-hot-middleware/client', 12 | './client' 13 | ]), 14 | output: { 15 | path: path.join(__dirname, 'static'), 16 | filename: 'bundle.js', 17 | publicPath: '/static/' 18 | }, 19 | plugins: filter([ 20 | HOT && new webpack.HotModuleReplacementPlugin(), 21 | HOT && new webpack.NamedModulesPlugin(), 22 | ! HOT && new webpack.optimize.UglifyJsPlugin() 23 | ]), 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.js$/, 28 | use: [ 29 | 'babel-loader', 30 | ], 31 | exclude: /node_modules/ 32 | }, 33 | { 34 | test: /\.less$/, 35 | use: [ 36 | 'style-loader', 37 | 'css-loader', 38 | { 39 | loader: 'postcss-loader', 40 | options: { 41 | plugins: () => filter([ 42 | ! HOT && require('autoprefixer') 43 | ]) 44 | } 45 | }, 46 | 'less-loader' 47 | ] 48 | } 49 | ] 50 | } 51 | }; 52 | --------------------------------------------------------------------------------