├── .babelrc ├── .gitignore ├── README.md ├── actions └── CartActions.js ├── components ├── Duck.js ├── DuckCart.js ├── DucksToBuy.js └── ShopSearch.js ├── constants └── ActionTypes.js ├── containers ├── App.js └── DucksApp.js ├── data └── getData.js ├── dist └── bundle.js ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── reducers └── cart.js ├── server.js ├── webpack.config.js └── webpack.config.min.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-object-rest-spread", "jsx-display-if"], 3 | "presets": ["es2015", "react"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Updated deps - Feb 2017 2 | 3 | Originally created - July 2015 4 | 5 | # react-with-redux-shop-ducks 6 | "searchable" shopping cart example with react, redux, and redux-form 7 | 8 | Wrote an accompanying [blog post](http://www.hartzis.me/functional-redux-ducks/). 9 | 10 | [Live example](http://hartzis.github.io/react-with-redux-shop-ducks/) 11 | 12 | ### key libs used 13 | 14 | #### react/redux 15 | - [redux](https://github.com/rackt/redux) - v3.6.0 16 | - [react-redux](https://github.com/rackt/react-redux) - v5.0.0 17 | - [redux-form](https://github.com/erikras/redux-form) - v3.0.5 18 | 19 | #### build 20 | - [babel](https://babeljs.io/) - v6.23.0 21 | - [webpack](https://webpack.js.org/) - v2.2.0 22 | - [webpack-dev-server](https://webpack.github.io/docs/webpack-dev-server.html) - v2.3.0 23 | 24 | #### jsx helpers 25 | - [display-if](https://github.com/craftsy/babel-plugin-jsx-display-if) - Allows conditional displaying via `
` 26 | 27 | 28 | ## Run Example 29 | 30 | `npm install` 31 | 32 | `npm start` 33 | -------------------------------------------------------------------------------- /actions/CartActions.js: -------------------------------------------------------------------------------- 1 | import {ADD_TO_CART, REMOVE_FROM_CART, SET_DUCKS, SET_LOADING, SET_QUERY} from '../constants/ActionTypes'; 2 | import {getDucks as getDucksData} from '../data/getData'; 3 | 4 | function createAction(type) { 5 | return (payload) => ({ type, payload }); 6 | } 7 | 8 | export const setQuery = createAction(SET_QUERY); 9 | 10 | export const addToCart = createAction(ADD_TO_CART); 11 | 12 | export const removeFromCart = createAction(REMOVE_FROM_CART); 13 | 14 | export const setLoading = createAction(SET_LOADING); 15 | 16 | export const setDucks = createAction(SET_DUCKS); 17 | 18 | export function getDucks(query) { 19 | return (dispatch, getState) => { 20 | const q = query || getState().cart.get('query'); 21 | // loading cart 22 | dispatch(setLoading(true)); 23 | // find cart items by query 24 | getDucksData({ query: q }, (ducks)=>{ 25 | dispatch(setDucks(ducks)); 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /components/Duck.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const addButtonStyles = { 5 | 'margin': '5px', 6 | 'padding': '5px', 7 | 'backgroundColor': 'lightgray', 8 | 'border': '2px solid gray', 9 | 'cursor': 'pointer', 10 | }; 11 | 12 | export default function Duck(props) { 13 | 14 | let {duck, inCart, addToCart} = props; 15 | let duckStyles = { 16 | 'border': 'solid gray 2px', 17 | 'textAlign': 'center', 18 | 'margin': '3px', 19 | 'padding': '3px', 20 | }; 21 | let duckTitleStyle = { 22 | 'maxWidth': '240px', 23 | 'textOverflow': 'ellipsis', 24 | 'whiteSpace': 'nowrap', 25 | 'overflow': 'hidden', 26 | }; 27 | return ( 28 |
29 | 30 |
31 |
{duck.title || duck.date_taken}
32 | 35 |
36 |
37 | ) 38 | } 39 | 40 | Duck.propTypes = { 41 | duck: PropTypes.shape({ 42 | date_taken: PropTypes.string, 43 | media: PropTypes.shape({ 44 | m: PropTypes.string, 45 | }), 46 | title: PropTypes.string, 47 | }), 48 | addToCart: PropTypes.func, 49 | inCart: PropTypes.bool, 50 | } 51 | -------------------------------------------------------------------------------- /components/DuckCart.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { addButtonStyles } from './Duck.js'; 4 | 5 | let duckInCartStyle = { 6 | 'padding': '3px', 7 | 'borderLeft': '2px solid gray', 8 | 'borderBottom': '2px solid gray', 9 | }; 10 | 11 | export default function DuckCart(props) { 12 | let {ducksInCart, removeFromCart} = props; 13 | let $renderedDucksInCart = ducksInCart.map((duck, idx)=>{ 14 | return ( 15 |
16 | {duck.title || duck.date_taken} 17 | 20 |
21 | ); 22 | }) 23 | return ( 24 |
25 |

Ducks in yer kart

26 | {$renderedDucksInCart} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/DucksToBuy.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Duck from './Duck'; 3 | 4 | function _isInCart(duck, ducksInCart) { 5 | let foundDuck = ducksInCart.filter((duckInCart)=>{ 6 | return duck.date_taken === duckInCart.date_taken && duck.title === duckInCart.title 7 | }); 8 | return foundDuck.length > 0; 9 | } 10 | 11 | export default function DuckCart(props) { 12 | 13 | const {ducks, ducksInCart, addToCart, loading} = props; 14 | 15 | return ( 16 |
17 | Ducks are loading... 18 |
19 | {ducks.map((duck, idx)=>())} 20 |
21 | Cart search term returned no items(ducks)... 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /components/ShopSearch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as CartActions from '../actions/CartActions'; 4 | 5 | function ShopSearch({query, onChange, onSubmit}) { 6 | 7 | return ( 8 |
9 |
10 | 11 | onChange(e.target.value)} /> 12 |
13 | 14 |
15 | ); 16 | } 17 | 18 | export default connect(({cart})=>({query: cart.get('query')}), {onChange: CartActions.setQuery})(ShopSearch); 19 | -------------------------------------------------------------------------------- /constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TO_CART = 'ADD_TO_CART'; 2 | export const REMOVE_FROM_CART = 'REMOVE_FROM_CART'; 3 | export const SET_DUCKS = 'SET_DUCKS'; 4 | export const SET_LOADING = 'SET_LOADING'; 5 | export const SET_QUERY = 'SET_QUERY'; 6 | -------------------------------------------------------------------------------- /containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import DucksApp from './DucksApp'; 3 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | import { Provider } from 'react-redux'; 6 | import cartReducer from '../reducers/cart'; 7 | 8 | const reducers = { 9 | cart: cartReducer, 10 | }; 11 | console.dir(reducers); 12 | const reducer = combineReducers(reducers); 13 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 14 | const store = createStore(reducer, composeEnhancers( 15 | applyMiddleware(thunk) 16 | )); 17 | 18 | export default class App extends Component { 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /containers/DucksApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import DucksToBuy from '../components/DucksToBuy'; 5 | import DuckCart from '../components/DuckCart'; 6 | import ShopSearch from '../components/ShopSearch'; 7 | import * as CartActions from '../actions/CartActions'; 8 | 9 | class DucksApp extends Component { 10 | 11 | constructor(props, context) { 12 | super(props, context); 13 | this._onSubmit = this._onSubmit.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | this.props.getDucks(); 18 | } 19 | 20 | _onSubmit(e) { 21 | e && e.preventDefault && e.preventDefault(); 22 | this.props.getDucks(); 23 | } 24 | 25 | render() { 26 | const { ducks, ducksInCart, addToCart, removeFromCart, loading } = this.props; 27 | return ( 28 |
29 |

Shop ducks with redux!

30 |
* some ducks might not actually be ducks
31 | 32 |
33 | 36 | 37 |
38 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | function mapStateToProps(state) { 49 | return { 50 | ducks: state.cart.get('ducks').toJS(), 51 | ducksInCart: state.cart.get('ducksInCart').toJS(), 52 | loading: state.cart.get('loading'), 53 | } 54 | } 55 | 56 | export default connect(mapStateToProps, CartActions)(DucksApp); 57 | -------------------------------------------------------------------------------- /data/getData.js: -------------------------------------------------------------------------------- 1 | export function getDucks({query}, passedInCallback) { 2 | 3 | // setup query 4 | let tags = query || ''; 5 | let tagmode = "any"; 6 | let format = "json"; 7 | 8 | let queryString = "tags=" + tags + "&tagmode=" + tagmode + "&format=" + format; 9 | 10 | let jsonCallback = function ( data ) { 11 | console.log('ducks-', data); 12 | 13 | let ducks = data.items; 14 | 15 | // remove from global scope 16 | delete window.jsonFlickrFeed; 17 | 18 | return passedInCallback(ducks); 19 | 20 | }; 21 | 22 | // Vanilla 23 | let jsonFlickrFeed = function ( data ) { 24 | jsonCallback( data ); 25 | } 26 | // put on global scope temporaralllyyyy 27 | window.jsonFlickrFeed = jsonFlickrFeed; 28 | let scr = document.createElement('script'); 29 | 30 | scr.src = 'http://api.flickr.com/services/feeds/photos_public.gne?callback=jsonFlickrFeed&' + queryString; 31 | document.body.appendChild(scr); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Shop Ducks 4 | 8 | 9 | 10 |
11 |
12 | github.com/hartzis/react-with-redux-shop-ducks by hartzis 13 |
14 |
15 |
16 |
17 | 18 | 19 | 27 | 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './containers/App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-with-redux-shop-ducks", 3 | "version": "1.0.0", 4 | "description": "shopping cart example with react and redux", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "build": "webpack", 9 | "build-min": "NODE_ENV=production webpack --config webpack.config.min.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/hartzis/react-with-redux-shop-ducks.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/hartzis/react-with-redux-shop-ducks/issues" 20 | }, 21 | "homepage": "https://github.com/hartzis/react-with-redux-shop-ducks#readme", 22 | "dependencies": { 23 | "immutable": "^3.7.4", 24 | "prop-types": "^15.6.1", 25 | "react": "^16.3.0", 26 | "react-dom": "^16.3.0", 27 | "react-redux": "^5.0.7", 28 | "redux": "^3.7.2", 29 | "redux-thunk": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.23.0", 33 | "babel-loader": "^6.2.0", 34 | "babel-plugin-transform-object-rest-spread": "^6.3.13", 35 | "babel-preset-es2015": "^6.3.13", 36 | "babel-preset-react": "^6.3.13", 37 | "babel-plugin-jsx-display-if": "^3.0.0", 38 | "node-libs-browser": "^0.5.2", 39 | "react-hot-loader": "^1.2.8", 40 | "webpack": "^2.2.0", 41 | "webpack-dev-server": "^2.3.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /reducers/cart.js: -------------------------------------------------------------------------------- 1 | import * as Im from 'immutable'; 2 | import {ADD_TO_CART, REMOVE_FROM_CART, SET_DUCKS, SET_LOADING, SET_QUERY} from '../constants/ActionTypes'; 3 | 4 | const initialState = Im.fromJS({ 5 | query: 'baby ducks', 6 | ducks: [], 7 | ducksInCart: [], 8 | loading: false, 9 | }); 10 | 11 | export default function cart(state = initialState, {type, payload}) { 12 | switch (type) { 13 | case SET_DUCKS: 14 | return state.merge({ 15 | 'ducks': payload, 16 | 'loading': false 17 | }); 18 | case SET_QUERY: 19 | return state.set('query', payload); 20 | case SET_LOADING: 21 | return state.set('loading', payload); 22 | case ADD_TO_CART: 23 | return state.update('ducksInCart', ducksInCart=>ducksInCart.push(Im.fromJS(payload))); 24 | case REMOVE_FROM_CART: 25 | return state.update('ducksInCart', (ducksInCart)=>{ 26 | return ducksInCart.filter(duck=>duck.get('link') !== payload.link); 27 | }); 28 | default: 29 | return state; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true, 9 | stats: { 10 | colors: true 11 | } 12 | }).listen(3000, 'localhost', function (err) { 13 | if (err) { 14 | console.log(err); 15 | } 16 | 17 | console.log('Listening at localhost:3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | './index' 8 | ], 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | publicPath: '/dist/' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | loader: 'babel-loader', 18 | resource: { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | } 22 | } 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /webpack.config.min.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | './index' 8 | ], 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | publicPath: '/dist/' 13 | }, 14 | plugins: [ 15 | new webpack.optimize.UglifyJsPlugin({minimize: true}), 16 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | loader: 'babel-loader', 22 | resource: { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | } 26 | } 27 | ] 28 | } 29 | }; 30 | --------------------------------------------------------------------------------