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