├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------