[
  {
    "path": ".firebaserc",
    "content": "{\n  \"projects\": {\n    \"default\": \"todo-redux-saga\"\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "#======================================\n# Directories\n#--------------------------------------\nbuild/\ndist/\ncoverage/\nnode_modules/\ntmp/\n\n\n#======================================\n# Extensions\n#--------------------------------------\n*.css\n*.gz\n*.local\n*.log\n*.rar\n*.tar\n*.zip\n\n\n#======================================\n# IDE generated\n#--------------------------------------\n.idea/\n.project\n*.iml\n\n\n#======================================\n# OS generated\n#--------------------------------------\n__MACOSX/\n.DS_Store\nThumbs.db\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2016 Richard Park (objectiv@gmail.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![CircleCI](https://circleci.com/gh/r-park/todo-redux-saga.svg?style=shield&circle-token=dc7e150ab97aab05db8f8da4b5874488bf8da0c6)](https://circleci.com/gh/r-park/todo-redux-saga)\n\n\n# A simple Todo app example built with [Create React App](https://github.com/facebookincubator/create-react-app), [React Redux](https://github.com/reactjs/react-redux), [Redux Saga](https://github.com/redux-saga/redux-saga), and Firebase\n\nTry the demo at <a href=\"https://todo-redux-saga.firebaseapp.com\" target=\"_blank\">todo-redux-saga.firebaseapp.com</a>.\n\n\n## Stack\n\n- Create React App\n- React Redux\n- React Router\n- React Router Redux\n- Redux Saga\n- Redux Devtools Extension for Chrome\n- Firebase SDK with OAuth authentication\n- Immutable\n- Reselect\n- SASS\n\n\n## Quick Start\n\n```shell\n$ git clone https://github.com/r-park/todo-redux-saga.git\n$ cd todo-redux-saga\n$ npm install\n$ npm start\n```\n\n## Deploying to Firebase\n#### Prerequisites:\n- Create a free Firebase account at https://firebase.google.com\n- Create a project from your [Firebase account console](https://console.firebase.google.com)\n- Configure the authentication providers for your Firebase project from your Firebase account console\n\n#### Configure this app with your project-specific details:\n```json\n// .firebaserc\n\n{\n  \"projects\": {\n    \"default\": \"your-project-id\"\n  }\n}\n```\n\n```javascript\n// src/firebase/config.js\n\nexport const firebaseConfig = {\n  apiKey: 'your api key',\n  authDomain: 'your-project-id.firebaseapp.com',\n  databaseURL: 'https://your-project-id.firebaseio.com',\n  storageBucket: 'your-project-id.appspot.com'\n};\n```\n\n#### Install firebase-tools:\n```shell\n$ npm install -g firebase-tools\n```\n\n#### Build and deploy the app:\n```shell\n$ npm run build\n$ firebase login\n$ firebase use default\n$ firebase deploy\n```\n\n\n## NPM Commands\n\n|Script|Description|\n|---|---|\n|`npm start`|Start webpack development server @ `localhost:3000`|\n|`npm run build`|Build the application to `./build` directory|\n|`npm test`|Test the application; watch for changes and retest|\n"
  },
  {
    "path": "circle.yml",
    "content": "machine:\n  node:\n    version: 8.1\n\ndependencies:\n  pre:\n    - rm -rf node_modules\n\ntest:\n  override:\n    - npm run build\n    - npm test\n\ndeployment:\n  production:\n    branch: master\n    commands:\n      - ./node_modules/.bin/firebase deploy --token $FIREBASE_TOKEN\n"
  },
  {
    "path": "firebase.json",
    "content": "{\n  \"database\": {\n    \"rules\": \"firebase.rules.json\"\n  },\n\n  \"hosting\": {\n    \"public\": \"build\",\n    \"headers\": [\n      {\n        \"source\": \"**/*\",\n        \"headers\": [\n          {\"key\": \"X-Content-Type-Options\", \"value\": \"nosniff\"},\n          {\"key\": \"X-Frame-Options\", \"value\": \"DENY\"},\n          {\"key\": \"X-UA-Compatible\", \"value\": \"ie=edge\"},\n          {\"key\": \"X-XSS-Protection\", \"value\": \"1; mode=block\"}\n        ]\n      },\n      {\n        \"source\": \"**/*.@(css|html|js|map)\",\n        \"headers\": [\n          {\"key\": \"Cache-Control\", \"value\": \"max-age=3600\"}\n        ]\n      }\n    ],\n    \"rewrites\": [\n      {\"source\": \"**\", \"destination\": \"/index.html\"}\n    ]\n  }\n}\n"
  },
  {
    "path": "firebase.rules.json",
    "content": "{\n  \"rules\": {\n    \"tasks\": {\n      \"$uid\": {\n        \".read\": \"auth !== null && auth.uid === $uid\",\n        \".write\": \"auth !== null && auth.uid === $uid\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"todo-redux-saga\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Todo app with React, Redux, Redux-Saga, and Firebase\",\n  \"homepage\": \"https://todo-redux-saga.firebaseapp.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/r-park/todo-redux-saga.git\"\n  },\n  \"author\": {\n    \"name\": \"Richard Park\",\n    \"email\": \"objectiv@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \">=8.1.4\"\n  },\n  \"scripts\": {\n    \"eject\": \"react-scripts eject\",\n    \"build\": \"run-s build.css build.js\",\n    \"build.css\": \"node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/\",\n    \"build.js\": \"cross-env NODE_PATH=. react-scripts build\",\n    \"start\": \"run-p start.css start.js\",\n    \"start.css\": \"npm run build.css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive\",\n    \"start.js\": \"cross-env NODE_PATH=. react-scripts start\",\n    \"test\": \"cross-env NODE_PATH=. react-scripts test --env=jsdom\",\n    \"test.ci\": \"cross-env CI=true NODE_PATH=. react-scripts test --env=jsdom\"\n  },\n  \"dependencies\": {\n    \"classnames\": \"^2.2.5\",\n    \"firebase\": \"^4.1.3\",\n    \"history\": \"^4.6.3\",\n    \"immutable\": \"^3.8.1\",\n    \"prop-types\": \"^15.5.10\",\n    \"react\": \"^15.6.1\",\n    \"react-dom\": \"^15.6.1\",\n    \"react-redux\": \"^5.0.5\",\n    \"react-router\": \"^4.1.1\",\n    \"react-router-dom\": \"^4.1.1\",\n    \"react-router-redux\": \"^5.0.0-alpha.6\",\n    \"react-scripts\": \"1.0.10\",\n    \"redux\": \"^3.7.1\",\n    \"redux-saga\": \"^0.15.4\",\n    \"reselect\": \"^3.0.1\"\n  },\n  \"devDependencies\": {\n    \"cross-env\": \"^5.0.1\",\n    \"enzyme\": \"^2.9.1\",\n    \"firebase-tools\": \"^3.9.1\",\n    \"minx\": \"r-park/minx.git\",\n    \"node-sass-chokidar\": \"0.0.3\",\n    \"npm-run-all\": \"^4.0.2\",\n    \"react-test-renderer\": \"^15.6.1\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"theme-color\" content=\"#000000\">\n\n    <title>Todo Redux Saga</title>\n\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\">\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\">\n\n    <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/icon?family=Material+Icons\">\n\n    <script src=\"https://use.typekit.net/kmv5qdr.js\"></script>\n    <script>try{Typekit.load({ async: true });}catch(e){}</script>\n  </head>\n\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"Todo Redux Saga\",\n  \"name\": \"Todo app with React, Redux, Redux-Saga, and Firebase\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"start_url\": \"./index.html\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "src/auth/actions.js",
    "content": "import firebase from 'firebase/app';\n\n\nexport const authActions = {\n  SIGN_IN: 'SIGN_IN',\n  SIGN_IN_FAILED: 'SIGN_IN_FAILED',\n  SIGN_IN_FULFILLED: 'SIGN_IN_FULFILLED',\n\n  SIGN_OUT: 'SIGN_OUT',\n  SIGN_OUT_FAILED: 'SIGN_OUT_FAILED',\n  SIGN_OUT_FULFILLED: 'SIGN_OUT_FULFILLED',\n\n\n  signIn: authProvider => ({\n    type: authActions.SIGN_IN,\n    payload: {authProvider}\n  }),\n\n  signInFailed: error => ({\n    type: authActions.SIGN_IN_FAILED,\n    payload: {error}\n  }),\n\n  signInFulfilled: authUser => ({\n    type: authActions.SIGN_IN_FULFILLED,\n    payload: {authUser}\n  }),\n\n  signInWithGithub: () => authActions.signIn(\n    new firebase.auth.GithubAuthProvider()\n  ),\n\n  signInWithGoogle: () => authActions.signIn(\n    new firebase.auth.GoogleAuthProvider()\n  ),\n\n  signInWithTwitter: () => authActions.signIn(\n    new firebase.auth.TwitterAuthProvider()\n  ),\n\n  signOut: () => ({\n    type: authActions.SIGN_OUT\n  }),\n\n  signOutFailed: error => ({\n    type: authActions.SIGN_OUT_FAILED,\n    payload: {error}\n  }),\n\n  signOutFulfilled: () => ({\n    type: authActions.SIGN_OUT_FULFILLED\n  })\n};\n"
  },
  {
    "path": "src/auth/auth.js",
    "content": "import { firebaseAuth } from 'src/firebase';\nimport { authActions } from './actions';\n\n\nexport function initAuth(dispatch) {\n  return new Promise((resolve, reject) => {\n    const unsubscribe = firebaseAuth.onAuthStateChanged(\n      authUser => {\n        if (authUser) {\n          dispatch(authActions.signInFulfilled(authUser));\n        }\n\n        resolve();\n        unsubscribe();\n      },\n\n      error => reject(error)\n    );\n  });\n}\n"
  },
  {
    "path": "src/auth/index.js",
    "content": "export { authActions } from './actions';\nexport { initAuth } from './auth';\nexport { authReducer } from './reducer';\nexport { authSagas } from './sagas';\nexport { getAuth, isAuthenticated } from './selectors';\n"
  },
  {
    "path": "src/auth/reducer.js",
    "content": "import { Record } from 'immutable';\nimport { authActions } from './actions';\n\n\nexport const AuthState = new Record({\n  authenticated: false,\n  uid: null,\n  user: null\n});\n\n\nexport function authReducer(state = new AuthState(), {payload, type}) {\n  switch (type) {\n    case authActions.SIGN_IN_FULFILLED:\n      return state.merge({\n        authenticated: true,\n        uid: payload.uid,\n        user: payload\n      });\n\n    case authActions.SIGN_OUT_FULFILLED:\n      return state.merge({\n        authenticated: false,\n        uid: null,\n        user: null\n      });\n\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "src/auth/sagas.js",
    "content": "import { call, fork, put, take } from 'redux-saga/effects';\nimport { firebaseAuth } from 'src/firebase';\nimport history from 'src/history';\nimport { authActions } from './actions';\n\n\nfunction* signIn(authProvider) {\n  try {\n    const authData = yield call([firebaseAuth, firebaseAuth.signInWithPopup], authProvider);\n    yield put(authActions.signInFulfilled(authData.user));\n    yield history.push('/');\n  }\n  catch (error) {\n    yield put(authActions.signInFailed(error));\n  }\n}\n\nfunction* signOut() {\n  try {\n    yield call([firebaseAuth, firebaseAuth.signOut]);\n    yield put(authActions.signOutFulfilled());\n    yield history.replace('/sign-in');\n  }\n  catch (error) {\n    yield put(authActions.signOutFailed(error));\n  }\n}\n\n\n//=====================================\n//  WATCHERS\n//-------------------------------------\n\nfunction* watchSignIn() {\n  while (true) {\n    let { payload } = yield take(authActions.SIGN_IN);\n    yield fork(signIn, payload.authProvider);\n  }\n}\n\nfunction* watchSignOut() {\n  while (true) {\n    yield take(authActions.SIGN_OUT);\n    yield fork(signOut);\n  }\n}\n\n\n//=====================================\n//  AUTH SAGAS\n//-------------------------------------\n\nexport const authSagas = [\n  fork(watchSignIn),\n  fork(watchSignOut)\n];\n"
  },
  {
    "path": "src/auth/selectors.js",
    "content": "import { createSelector } from 'reselect';\n\n\nexport function isAuthenticated(state) {\n  return state.auth.authenticated;\n}\n\n\n//=====================================\n//  MEMOIZED SELECTORS\n//-------------------------------------\n\nexport const getAuth = createSelector(\n  state => state.auth,\n  auth => auth.toJS()\n);\n"
  },
  {
    "path": "src/firebase/config.js",
    "content": "export const firebaseConfig = {\n  apiKey: 'AIzaSyCUll5AyYba1XL8NDYKZ51RGt90KofQo6c',\n  authDomain: 'todo-redux-saga.firebaseapp.com',\n  databaseURL: 'https://todo-redux-saga.firebaseio.com',\n  storageBucket: 'todo-redux-saga.appspot.com'\n};\n"
  },
  {
    "path": "src/firebase/firebase-list.js",
    "content": "import { firebaseDb } from './firebase';\n\n\nexport class FirebaseList {\n  constructor(actions, modelClass) {\n    this._actions = actions;\n    this._modelClass = modelClass;\n  }\n\n  get path() {\n    return this._path;\n  }\n\n  set path(value) {\n    this._path = value;\n  }\n\n  push(value) {\n    return new Promise((resolve, reject) => {\n      firebaseDb.ref(this.path)\n        .push(value, error => error ? reject(error) : resolve());\n    });\n  }\n\n  remove(key) {\n    return new Promise((resolve, reject) => {\n      firebaseDb.ref(`${this.path}/${key}`)\n        .remove(error => error ? reject(error) : resolve());\n    });\n  }\n\n  update(key, value) {\n    return new Promise((resolve, reject) => {\n      firebaseDb.ref(`${this.path}/${key}`)\n        .update(value, error => error ? reject(error) : resolve());\n    });\n  }\n\n  subscribe(emit) {\n    let ref = firebaseDb.ref(this.path);\n    let initialized = false;\n    let list = [];\n\n    ref.once('value', () => {\n      initialized = true;\n      emit(this._actions.onLoad(list));\n    });\n\n    ref.on('child_added', snapshot => {\n      if (initialized) {\n        emit(this._actions.onAdd(this.unwrapSnapshot(snapshot)));\n      }\n      else {\n        list.push(this.unwrapSnapshot(snapshot));\n      }\n    });\n\n    ref.on('child_changed', snapshot => {\n      emit(this._actions.onChange(this.unwrapSnapshot(snapshot)));\n    });\n\n    ref.on('child_removed', snapshot => {\n      emit(this._actions.onRemove(this.unwrapSnapshot(snapshot)));\n    });\n\n    return () => ref.off();\n  }\n\n  unwrapSnapshot(snapshot) {\n    let attrs = snapshot.val();\n    attrs.key = snapshot.key;\n    return new this._modelClass(attrs);\n  }\n}\n"
  },
  {
    "path": "src/firebase/firebase.js",
    "content": "import firebase from 'firebase/app';\n\nimport 'firebase/auth';\nimport 'firebase/database';\n\nimport { firebaseConfig } from './config';\n\n\nexport const firebaseApp = firebase.initializeApp(firebaseConfig);\nexport const firebaseAuth = firebase.auth();\nexport const firebaseDb = firebase.database();\n"
  },
  {
    "path": "src/firebase/index.js",
    "content": "export { firebaseApp, firebaseAuth, firebaseDb } from './firebase';\nexport { FirebaseList } from './firebase-list';\n"
  },
  {
    "path": "src/history.js",
    "content": "import createHistory from 'history/createBrowserHistory';\n\n\nexport default createHistory();\n"
  },
  {
    "path": "src/index.js",
    "content": "import './views/styles/styles.css';\n\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } from 'react-redux';\nimport { ConnectedRouter } from 'react-router-redux';\n\nimport { initAuth } from './auth';\nimport history from './history';\nimport configureStore from './store';\nimport App from './views/app';\nimport registerServiceWorker from './register-service-worker';\n\n\nconst store = configureStore();\nconst rootElement = document.getElementById('root');\n\n\nfunction render(Component) {\n  ReactDOM.render(\n    <Provider store={store}>\n      <ConnectedRouter history={history}>\n        <div>\n          <Component/>\n        </div>\n      </ConnectedRouter>\n    </Provider>,\n    rootElement\n  );\n}\n\n\nif (module.hot) {\n  module.hot.accept('./views/app', () => {\n    render(require('./views/app').default);\n  })\n}\n\n\nregisterServiceWorker();\n\n\ninitAuth(store.dispatch)\n  .then(() => render(App))\n  .catch(error => console.error(error));\n"
  },
  {
    "path": "src/reducers.js",
    "content": "import { combineReducers } from 'redux';\nimport { routerReducer } from 'react-router-redux';\nimport { authReducer } from './auth';\nimport { tasksReducer } from './tasks';\n\n\nexport default combineReducers({\n  auth: authReducer,\n  routing: routerReducer,\n  tasks: tasksReducer\n});\n"
  },
  {
    "path": "src/register-service-worker.js",
    "content": "// In production, we register a service worker to serve assets from local cache.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on the \"N+1\" visit to a page, since previously\n// cached resources are updated in the background.\n\n// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.\n// This link also includes instructions on opting out of this behavior.\n\nconst isLocalhost = Boolean(\n  window.location.hostname === 'localhost' ||\n    // [::1] is the IPv6 localhost address.\n    window.location.hostname === '[::1]' ||\n    // 127.0.0.1/8 is considered localhost for IPv4.\n    window.location.hostname.match(\n      /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n    )\n);\n\nexport default function register() {\n  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n    // The URL constructor is available in all browsers that support SW.\n    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);\n    if (publicUrl.origin !== window.location.origin) {\n      // Our service worker won't work if PUBLIC_URL is on a different origin\n      // from what our page is served on. This might happen if a CDN is used to\n      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374\n      return;\n    }\n\n    window.addEventListener('load', () => {\n      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n      if (!isLocalhost) {\n        // Is not local host. Just register service worker\n        registerValidSW(swUrl);\n      } else {\n        // This is running on localhost. Lets check if a service worker still exists or not.\n        checkValidServiceWorker(swUrl);\n      }\n    });\n  }\n}\n\nfunction registerValidSW(swUrl) {\n  navigator.serviceWorker\n    .register(swUrl)\n    .then(registration => {\n      registration.onupdatefound = () => {\n        const installingWorker = registration.installing;\n        installingWorker.onstatechange = () => {\n          if (installingWorker.state === 'installed') {\n            if (navigator.serviceWorker.controller) {\n              // At this point, the old content will have been purged and\n              // the fresh content will have been added to the cache.\n              // It's the perfect time to display a \"New content is\n              // available; please refresh.\" message in your web app.\n              console.log('New content is available; please refresh.');\n            } else {\n              // At this point, everything has been precached.\n              // It's the perfect time to display a\n              // \"Content is cached for offline use.\" message.\n              console.log('Content is cached for offline use.');\n            }\n          }\n        };\n      };\n    })\n    .catch(error => {\n      console.error('Error during service worker registration:', error);\n    });\n}\n\nfunction checkValidServiceWorker(swUrl) {\n  // Check if the service worker can be found. If it can't reload the page.\n  fetch(swUrl)\n    .then(response => {\n      // Ensure service worker exists, and that we really are getting a JS file.\n      if (\n        response.status === 404 ||\n        response.headers.get('content-type').indexOf('javascript') === -1\n      ) {\n        // No service worker found. Probably a different app. Reload the page.\n        navigator.serviceWorker.ready.then(registration => {\n          registration.unregister().then(() => {\n            window.location.reload();\n          });\n        });\n      } else {\n        // Service worker found. Proceed as normal.\n        registerValidSW(swUrl);\n      }\n    })\n    .catch(() => {\n      console.log(\n        'No internet connection found. App is running in offline mode.'\n      );\n    });\n}\n\nexport function unregister() {\n  if ('serviceWorker' in navigator) {\n    navigator.serviceWorker.ready.then(registration => {\n      registration.unregister();\n    });\n  }\n}\n"
  },
  {
    "path": "src/sagas.js",
    "content": "import { all } from 'redux-saga/effects'\nimport { authSagas } from './auth';\nimport { taskSagas } from './tasks';\n\n\nexport default function* sagas() {\n  yield all([\n    ...authSagas,\n    ...taskSagas\n  ]);\n}\n"
  },
  {
    "path": "src/store.js",
    "content": "import { routerMiddleware } from 'react-router-redux';\nimport { applyMiddleware, compose, createStore } from 'redux';\nimport createSagaMiddleware from 'redux-saga';\nimport history from './history';\nimport reducers from './reducers';\nimport sagas from './sagas';\n\n\nexport default function configureStore() {\n  const sagaMiddleware = createSagaMiddleware();\n  let middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history));\n\n  if (process.env.NODE_ENV !== 'production') {\n    const devToolsExtension = window.devToolsExtension;\n    if (typeof devToolsExtension === 'function') {\n      middleware = compose(middleware, devToolsExtension());\n    }\n  }\n\n  const store = createStore(reducers, middleware);\n  sagaMiddleware.run(sagas);\n\n  if (module.hot) {\n    module.hot.accept('./reducers', () => {\n      store.replaceReducer(require('./reducers').default);\n    });\n  }\n\n  return store;\n}\n"
  },
  {
    "path": "src/tasks/actions.js",
    "content": "export const taskActions = {\n  CREATE_TASK: 'CREATE_TASK',\n  CREATE_TASK_FAILED: 'CREATE_TASK_FAILED',\n  CREATE_TASK_FULFILLED: 'CREATE_TASK_FULFILLED',\n\n  REMOVE_TASK: 'REMOVE_TASK',\n  REMOVE_TASK_FAILED: 'REMOVE_TASK_FAILED',\n  REMOVE_TASK_FULFILLED: 'REMOVE_TASK_FULFILLED',\n\n  UPDATE_TASK: 'UPDATE_TASK',\n  UPDATE_TASK_FAILED: 'UPDATE_TASK_FAILED',\n  UPDATE_TASK_FULFILLED: 'UPDATE_TASK_FULFILLED',\n\n  FILTER_TASKS: 'FILTER_TASKS',\n  LOAD_TASKS_FULFILLED: 'LOAD_TASKS_FULFILLED',\n\n\n  createTask: title => ({\n    type: taskActions.CREATE_TASK,\n    payload: {task: {title, completed: false}}\n  }),\n\n  createTaskFailed: error => ({\n    type: taskActions.CREATE_TASK_FAILED,\n    payload: {error}\n  }),\n\n  createTaskFulfilled: task => ({\n    type: taskActions.CREATE_TASK_FULFILLED,\n    payload: {task}\n  }),\n\n  removeTask: task => ({\n    type: taskActions.REMOVE_TASK,\n    payload: {task}\n  }),\n\n  removeTaskFailed: error => ({\n    type: taskActions.REMOVE_TASK_FAILED,\n    payload: {error}\n  }),\n\n  removeTaskFulfilled: task => ({\n    type: taskActions.REMOVE_TASK_FULFILLED,\n    payload: {task}\n  }),\n\n  updateTask: (task, changes) => ({\n    type: taskActions.UPDATE_TASK,\n    payload: {task, changes}\n  }),\n\n  updateTaskFailed: error => ({\n    type: taskActions.UPDATE_TASK_FAILED,\n    payload: {error}\n  }),\n\n  updateTaskFulfilled: task => ({\n    type: taskActions.UPDATE_TASK_FULFILLED,\n    payload: {task}\n  }),\n\n  filterTasks: filterType => ({\n    type: taskActions.FILTER_TASKS,\n    payload: {filterType}\n  }),\n\n  loadTasksFulfilled: tasks => ({\n    type: taskActions.LOAD_TASKS_FULFILLED,\n    payload: {tasks}\n  })\n};\n"
  },
  {
    "path": "src/tasks/index.js",
    "content": "export { taskActions } from './actions';\nexport { tasksReducer } from './reducer';\nexport { taskSagas } from './sagas';\nexport { getVisibleTasks } from './selectors';\n"
  },
  {
    "path": "src/tasks/reducer.js",
    "content": "import { List, Record } from 'immutable';\nimport { taskActions } from './actions';\n\n\nexport const TasksState = new Record({\n  filter: '',\n  list: new List()\n});\n\n\nexport function tasksReducer(state = new TasksState(), {payload, type}) {\n  switch (type) {\n    case taskActions.CREATE_TASK_FULFILLED:\n      return state.set('list', state.list.unshift(payload.task));\n\n    case taskActions.FILTER_TASKS:\n      return state.set('filter', payload.filterType || '');\n\n    case taskActions.LOAD_TASKS_FULFILLED:\n      return state.set('list', new List(payload.tasks.reverse()));\n\n    case taskActions.REMOVE_TASK_FULFILLED:\n      return state.set('list', state.list.filter(task => {\n        return task.key !== payload.task.key;\n      }));\n\n    case taskActions.UPDATE_TASK_FULFILLED:\n      return state.set('list', state.list.map(task => {\n        return task.key === payload.task.key ? payload.task : task;\n      }));\n\n    default:\n      return state;\n  }\n}\n"
  },
  {
    "path": "src/tasks/sagas.js",
    "content": "import { LOCATION_CHANGE } from 'react-router-redux';\nimport { eventChannel } from 'redux-saga';\nimport { call, cancel, fork, put, take } from 'redux-saga/effects';\nimport { authActions } from 'src/auth';\nimport { taskActions } from './actions';\nimport { taskList } from './task-list';\n\n\nfunction subscribe() {\n  return eventChannel(emit => taskList.subscribe(emit));\n}\n\nfunction* read() {\n  const channel = yield call(subscribe);\n  while (true) {\n    let action = yield take(channel);\n    yield put(action);\n  }\n}\n\nfunction* write(context, method, onError, ...params) {\n  try {\n    yield call([context, method], ...params);\n  }\n  catch (error) {\n    yield put(onError(error));\n  }\n}\n\nconst createTask = write.bind(null, taskList, taskList.push, taskActions.createTaskFailed);\nconst removeTask = write.bind(null, taskList, taskList.remove, taskActions.removeTaskFailed);\nconst updateTask = write.bind(null, taskList, taskList.update, taskActions.updateTaskFailed);\n\n\n//=====================================\n//  WATCHERS\n//-------------------------------------\n\nfunction* watchAuthentication() {\n  while (true) {\n    let { payload } = yield take(authActions.SIGN_IN_FULFILLED);\n\n    taskList.path = `tasks/${payload.authUser.uid}`;\n    const job = yield fork(read);\n\n    yield take([authActions.SIGN_OUT_FULFILLED]);\n    yield cancel(job);\n  }\n}\n\nfunction* watchCreateTask() {\n  while (true) {\n    let { payload } = yield take(taskActions.CREATE_TASK);\n    yield fork(createTask, payload.task);\n  }\n}\n\nfunction* watchLocationChange() {\n  while (true) {\n    let { payload } = yield take(LOCATION_CHANGE);\n    if (payload.pathname === '/') {\n      const params = new URLSearchParams(payload.search);\n      const filter = params.get('filter');\n      yield put(taskActions.filterTasks(filter));\n    }\n  }\n}\n\nfunction* watchRemoveTask() {\n  while (true) {\n    let { payload } = yield take(taskActions.REMOVE_TASK);\n    yield fork(removeTask, payload.task.key);\n  }\n}\n\nfunction* watchUpdateTask() {\n  while (true) {\n    let { payload } = yield take(taskActions.UPDATE_TASK);\n    yield fork(updateTask, payload.task.key, payload.changes);\n  }\n}\n\n\n//=====================================\n//  TASK SAGAS\n//-------------------------------------\n\nexport const taskSagas = [\n  fork(watchAuthentication),\n  fork(watchCreateTask),\n  fork(watchLocationChange),\n  fork(watchRemoveTask),\n  fork(watchUpdateTask)\n];\n"
  },
  {
    "path": "src/tasks/selectors.js",
    "content": "import { createSelector } from 'reselect';\n\n\nexport function getTasks(state) {\n  return state.tasks;\n}\n\nexport function getTaskFilter(state) {\n  return getTasks(state).filter;\n}\n\nexport function getTaskList(state) {\n  return getTasks(state).list;\n}\n\n\n//=====================================\n//  MEMOIZED SELECTORS\n//-------------------------------------\n\nexport const getVisibleTasks = createSelector(\n  getTaskFilter,\n  getTaskList,\n  (filter, taskList) => {\n    switch (filter) {\n      case 'active':\n        return taskList.filter(task => !task.completed);\n\n      case 'completed':\n        return taskList.filter(task => task.completed);\n\n      default:\n        return taskList;\n    }\n  }\n);\n"
  },
  {
    "path": "src/tasks/task-list.js",
    "content": "import { FirebaseList } from 'src/firebase';\nimport { taskActions } from './actions';\nimport { Task } from './task';\n\n\nexport const taskList = new FirebaseList({\n  onAdd: taskActions.createTaskFulfilled,\n  onChange: taskActions.updateTaskFulfilled,\n  onLoad: taskActions.loadTasksFulfilled,\n  onRemove: taskActions.removeTaskFulfilled\n}, Task);\n"
  },
  {
    "path": "src/tasks/task.js",
    "content": "import { Record } from 'immutable';\n\n\nexport const Task = new Record({\n  completed: false,\n  key: null,\n  title: null\n});\n"
  },
  {
    "path": "src/views/app/app.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { withRouter } from 'react-router-dom';\n\nimport { authActions, getAuth } from 'src/auth';\nimport Header from '../components/header';\nimport RequireAuthRoute from '../components/require-auth-route';\nimport RequireUnauthRoute from '../components/require-unauth-route';\nimport SignInPage from '../pages/sign-in';\nimport TasksPage from '../pages/tasks';\n\n\nconst App = ({authenticated, signOut}) => (\n  <div>\n    <Header\n      authenticated={authenticated}\n      signOut={signOut}\n    />\n\n    <main>\n      <RequireAuthRoute authenticated={authenticated} exact path=\"/\" component={TasksPage}/>\n      <RequireUnauthRoute authenticated={authenticated} path=\"/sign-in\" component={SignInPage}/>\n    </main>\n  </div>\n);\n\nApp.propTypes = {\n  authenticated: PropTypes.bool.isRequired,\n  signOut: PropTypes.func.isRequired\n};\n\n\n//=====================================\n//  CONNECT\n//-------------------------------------\n\nconst mapStateToProps = getAuth;\n\nconst mapDispatchToProps = {\n  signOut: authActions.signOut\n};\n\nexport default withRouter(\n  connect(\n    mapStateToProps,\n    mapDispatchToProps\n  )(App)\n);\n"
  },
  {
    "path": "src/views/app/index.js",
    "content": "export { default } from './app';\n"
  },
  {
    "path": "src/views/components/button/button.js",
    "content": "import React from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\n\nimport './button.css';\n\n\nconst Button = ({children, className, onClick, type = 'button'}) => {\n  const cssClasses = classNames('btn', className);\n  return (\n    <button className={cssClasses} onClick={onClick} type={type}>\n      {children}\n    </button>\n  );\n};\n\nButton.propTypes = {\n  children: PropTypes.node,\n  className: PropTypes.string,\n  onClick: PropTypes.func,\n  type: PropTypes.oneOf(['button', 'reset', 'submit'])\n};\n\n\nexport default Button;\n"
  },
  {
    "path": "src/views/components/button/button.scss",
    "content": "@import 'views/styles/shared';\n\n\n.btn {\n  @include button-base;\n  outline: none;\n  border: 0;\n  padding: 0;\n  overflow: hidden;\n  transform: translate(0, 0);\n  background: transparent;\n}\n\n.btn--icon {\n  border-radius: 40px;\n  padding: 8px;\n  width: 40px;\n  height: 40px;\n}\n"
  },
  {
    "path": "src/views/components/button/button.spec.js",
    "content": "import React from 'react';\nimport { render, shallow } from 'enzyme';\nimport Button from './button';\n\n\ndescribe('Button', () => {\n  it('should render a button with text node', () => {\n    const wrapper = render(<Button>Foo</Button>);\n    const button = wrapper.find('button');\n\n    expect(button.length).toBe(1);\n    expect(button.text()).toBe('Foo');\n  });\n\n  it('should render a button with child element', () => {\n    const wrapper = shallow(<Button><span>Foo</span></Button>);\n    const button = wrapper.find('button');\n\n    expect(button.length).toBe(1);\n    expect(button.contains(<span>Foo</span>)).toBe(true);\n  });\n\n  it('should set default className', () => {\n    const wrapper = render(<Button />);\n    const button = wrapper.find('button');\n\n    expect(button.hasClass('btn')).toBe(true);\n  });\n\n  it('should add provided props.className', () => {\n    const wrapper = render(<Button className=\"foo bar\" />);\n    const button = wrapper.find('button');\n\n    expect(button.hasClass('btn foo bar')).toBe(true);\n  });\n\n  it('should set type=button by default', () => {\n    const wrapper = render(<Button />);\n    const button = wrapper.find('button');\n\n    expect(button.attr('type')).toBe('button');\n  });\n\n  it('should set type with provided props.type', () => {\n    const wrapper = render(<Button type=\"submit\" />);\n    const button = wrapper.find('button');\n\n    expect(button.attr('type')).toBe('submit');\n  });\n\n  it('should set onClick with provided props.onClick', () => {\n    const handleClick = jasmine.createSpy('handleClick');\n    const wrapper = shallow(<Button onClick={handleClick} />);\n\n    wrapper.simulate('click');\n\n    expect(handleClick).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "src/views/components/button/index.js",
    "content": "export { default } from './button';\n"
  },
  {
    "path": "src/views/components/github-logo/github-logo.js",
    "content": "import React from 'react';\n\n\nexport default function GitHubLogo() {\n  return (\n    <svg viewBox=\"0 0 20 20\">\n      <path d=\"M10 0C4.5 0 0 4.5 0 10c0 4.4 2.9 8.2 6.8 9.5.5.1.7-.2.7-.5v-1.9c-2.5.5-3.2-.6-3.4-1.1-.1-.3-.6-1.2-1-1.4-.4-.2-.9-.6 0-.7.8 0 1.3.7 1.5 1 .9 1.5 2.4 1.1 3 .9.1-.6.4-1.1.6-1.3-2.2-.3-4.6-1.2-4.6-5 0-1.1.4-2 1-2.7 0-.3-.4-1.3.2-2.7 0 0 .8-.3 2.8 1 .7-.2 1.6-.3 2.4-.3s1.7.1 2.5.3c1.9-1.3 2.8-1 2.8-1 .5 1.4.2 2.4.1 2.7.6.7 1 1.6 1 2.7 0 3.8-2.3 4.7-4.6 4.9.4.3.7.9.7 1.9v2.8c0 .3.2.6.7.5 4-1.3 6.8-5.1 6.8-9.5C20 4.5 15.5 0 10 0z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/views/components/github-logo/index.js",
    "content": "export { default } from './github-logo';\n"
  },
  {
    "path": "src/views/components/header/header.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport Button from '../button';\nimport GitHubLogo from '../github-logo';\n\nimport './header.css';\n\n\nconst Header = ({authenticated, signOut}) => (\n  <header className=\"header\">\n    <div className=\"g-row\">\n      <div className=\"g-col\">\n        <h1 className=\"header__title\">Todo Redux Saga</h1>\n\n        <ul className=\"header__actions\">\n          {authenticated ? <li><Button onClick={signOut}>Sign out</Button></li> : null}\n          <li>\n            <a className=\"link link--github\" href=\"https://github.com/r-park/todo-redux-saga\">\n              <GitHubLogo />\n            </a>\n          </li>\n        </ul>\n      </div>\n    </div>\n  </header>\n);\n\nHeader.propTypes = {\n  authenticated: PropTypes.bool.isRequired,\n  signOut: PropTypes.func.isRequired\n};\n\n\nexport default Header;\n"
  },
  {
    "path": "src/views/components/header/header.scss",
    "content": "@import 'views/styles/shared';\n\n\n.header {\n  padding: 10px 0;\n  height: 60px;\n  overflow: hidden;\n  line-height: 40px;\n}\n\n.header__title {\n  float: left;\n  font-size: rem(14px);\n  font-weight: 400;\n  text-rendering: auto;\n  transform: translate(0,0);\n\n  &:before {\n    padding-right: 5px;\n    color: #fff;\n    line-height: 20px;\n  }\n}\n\n.header__actions {\n  @include clearfix;\n  float: right;\n  padding: 8px 0;\n  line-height: 24px;\n\n  li {\n    float: left;\n    list-style: none;\n\n    &:last-child {\n      margin-left: 12px;\n      padding-left: 12px;\n      border-left: 1px solid #333;\n    }\n\n    &:first-child {\n      border: none;\n    }\n  }\n\n  .btn {\n    display: block;\n    margin: 0;\n    color: #999;\n    font-size: rem(14px);\n    line-height: 24px;\n  }\n\n  .link {\n    display: block;\n    fill: #98999a;\n    transform: translate(0, 0);\n  }\n\n  .link--github {\n    padding-top: 1px;\n    width: 22px;\n    height: 24px;\n  }\n}\n"
  },
  {
    "path": "src/views/components/header/index.js",
    "content": "export { default } from './header';\n"
  },
  {
    "path": "src/views/components/icon/icon.js",
    "content": "import React from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\n\n\nconst Icon = ({className, name}) => {\n  const cssClasses = classNames('material-icons', className);\n  return <span className={cssClasses}>{name}</span>;\n};\n\nIcon.propTypes = {\n  className: PropTypes.string,\n  name: PropTypes.string.isRequired\n};\n\n\nexport default Icon;\n"
  },
  {
    "path": "src/views/components/icon/icon.spec.js",
    "content": "import React from 'react';\nimport { render, shallow } from 'enzyme';\nimport Icon from './icon';\n\n\ndescribe('Icon', () => {\n  it('should render an icon', () => {\n    const wrapper = shallow(<Icon name=\"play\" />);\n    expect(wrapper.contains(<span className=\"material-icons\">play</span>)).toBe(true);\n  });\n\n  it('should add provided props.className', () => {\n    const wrapper = render(<Icon className=\"foo bar\" name=\"play\" />);\n    const icon = wrapper.find('span');\n\n    expect(icon.hasClass('material-icons foo bar')).toBe(true);\n  });\n});\n"
  },
  {
    "path": "src/views/components/icon/index.js",
    "content": "export { default } from './icon';\n"
  },
  {
    "path": "src/views/components/require-auth-route/index.js",
    "content": "export { default } from './require-auth-route';\n"
  },
  {
    "path": "src/views/components/require-auth-route/require-auth-route.js",
    "content": "import React from 'react';\nimport { Route, Redirect } from 'react-router-dom'\n\n\nconst RequireAuthRoute = ({component: Component, authenticated, ...rest}) => (\n  <Route\n    {...rest}\n    render={props => {\n      return authenticated ? (\n        <Component {...props}/>\n      ) : (\n        <Redirect to={{\n          pathname: '/sign-in',\n          state: {from: props.location}\n        }}/>\n      )\n    }}\n  />\n);\n\n\nexport default RequireAuthRoute;\n"
  },
  {
    "path": "src/views/components/require-unauth-route/index.js",
    "content": "export { default } from './require-unauth-route';\n"
  },
  {
    "path": "src/views/components/require-unauth-route/require-unauth-route.js",
    "content": "import React from 'react';\nimport { Route, Redirect } from 'react-router-dom'\n\n\nconst RequireUnauthRoute = ({component: Component, authenticated, ...rest}) => (\n  <Route\n    {...rest}\n    render={props => {\n      return authenticated ? (\n        <Redirect to={{\n          pathname: '/',\n          state: {from: props.location}\n        }}/>\n      ) : (\n        <Component {...props}/>\n      )\n    }}\n  />\n);\n\n\nexport default RequireUnauthRoute;\n"
  },
  {
    "path": "src/views/components/task-filters/index.js",
    "content": "export { default } from './task-filters';\n"
  },
  {
    "path": "src/views/components/task-filters/task-filters.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { NavLink } from 'react-router-dom';\n\nimport './task-filters.css';\n\n\nconst TaskFilters = ({filter}) => (\n  <ul className=\"task-filters\">\n    <li><NavLink isActive={() => !filter} to=\"/\">View All</NavLink></li>\n    <li><NavLink isActive={() => filter === 'active'} to={{pathname: '/', search: '?filter=active'}}>Active</NavLink></li>\n    <li><NavLink isActive={() => filter === 'completed'} to={{pathname: '/', search: '?filter=completed'}}>Completed</NavLink></li>\n  </ul>\n);\n\nTaskFilters.propTypes = {\n  filter: PropTypes.string\n};\n\n\nexport default TaskFilters;\n"
  },
  {
    "path": "src/views/components/task-filters/task-filters.scss",
    "content": "@import 'views/styles/shared';\n\n\n.task-filters {\n  @include clearfix;\n  margin-bottom: 45px;\n  padding-left: 1px;\n  font-size: rem(16px);\n  line-height: 24px;\n  list-style-type: none;\n\n  @include media-query(540) {\n    margin-bottom: 55px;\n  }\n\n  li {\n    float: left;\n\n    &:not(:first-child) {\n      margin-left: 12px;\n    }\n\n    &:not(:first-child):before {\n      padding-right: 12px;\n      content: '/';\n      font-weight: 300;\n    }\n  }\n\n  a {\n    color: #999;\n    text-decoration: none;\n\n    &.active {\n      color: #fff;\n    }\n  }\n}\n"
  },
  {
    "path": "src/views/components/task-form/index.js",
    "content": "export { default } from './task-form';\n"
  },
  {
    "path": "src/views/components/task-form/task-form.js",
    "content": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\n\nimport './task-form.css';\n\n\nexport class TaskForm extends Component {\n  static propTypes = {\n    handleSubmit: PropTypes.func.isRequired\n  };\n\n  constructor() {\n    super(...arguments);\n\n    this.state = {title: ''};\n\n    this.handleChange = this.handleChange.bind(this);\n    this.handleKeyUp = this.handleKeyUp.bind(this);\n    this.handleSubmit = this.handleSubmit.bind(this);\n  }\n\n  clearInput() {\n    this.setState({title: ''});\n  }\n\n  handleChange(event) {\n    this.setState({title: event.target.value});\n  }\n\n  handleKeyUp(event) {\n    if (event.keyCode === 27) this.clearInput();\n  }\n\n  handleSubmit(event) {\n    event.preventDefault();\n    const title = this.state.title.trim();\n    if (title.length) this.props.handleSubmit(title);\n    this.clearInput();\n  }\n\n  render() {\n    return (\n      <form className=\"task-form\" onSubmit={this.handleSubmit} noValidate>\n        <input\n          autoComplete=\"off\"\n          autoFocus\n          className=\"task-form__input\"\n          maxLength=\"64\"\n          onChange={this.handleChange}\n          onKeyUp={this.handleKeyUp}\n          placeholder=\"What needs to be done?\"\n          type=\"text\"\n          value={this.state.title}\n        />\n      </form>\n    );\n  }\n}\n\n\nexport default TaskForm;\n"
  },
  {
    "path": "src/views/components/task-form/task-form.scss",
    "content": "@import 'views/styles/shared';\n\n\n.task-form {\n  margin: 40px 0 10px;\n\n  @include media-query(540) {\n    margin: 80px 0 20px;\n  }\n}\n\n.task-form__input {\n  outline: none;\n  border: 0;\n  border-bottom: 1px dotted #666;\n  border-radius: 0;\n  padding: 0 0 5px 0;\n  width: 100%;\n  height: 50px;\n  font-family: inherit;\n  font-size: rem(24px);\n  font-weight: 300;\n  color: #fff;\n  background: transparent;\n\n  @include media-query(540) {\n    height: 61px;\n    font-size: rem(32px);\n  }\n\n  &::placeholder {\n    color: #999;\n    opacity: 1; // firefox native placeholder style has opacity < 1\n  }\n\n  &:focus::placeholder {\n    color: #777;\n    opacity: 1;\n  }\n\n  // webkit input doesn't inherit font-smoothing from ancestors\n  -webkit-font-smoothing: antialiased;\n\n  // remove `x`\n  &::-ms-clear {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src/views/components/task-item/index.js",
    "content": "export { default } from './task-item';\n"
  },
  {
    "path": "src/views/components/task-item/task-item.js",
    "content": "import React, { Component } from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport Button from '../button';\nimport Icon from '../icon';\n\nimport './task-item.css';\n\n\nexport class TaskItem extends Component {\n  constructor() {\n    super(...arguments);\n\n    this.state = {editing: false};\n\n    this.edit = this.edit.bind(this);\n    this.handleKeyUp = this.handleKeyUp.bind(this);\n    this.remove = this.remove.bind(this);\n    this.save = this.save.bind(this);\n    this.stopEditing = this.stopEditing.bind(this);\n    this.toggleStatus = this.toggleStatus.bind(this);\n  }\n\n  edit() {\n    this.setState({editing: true});\n  }\n\n  handleKeyUp(event) {\n    if (event.keyCode === 13) {\n      this.save(event);\n    }\n    else if (event.keyCode === 27) {\n      this.stopEditing();\n    }\n  }\n\n  remove() {\n    this.props.removeTask(this.props.task);\n  }\n\n  save(event) {\n    if (this.state.editing) {\n      const { task } = this.props;\n      const title = event.target.value.trim();\n\n      if (title.length && title !== task.title) {\n        this.props.updateTask(task, {title});\n      }\n\n      this.stopEditing();\n    }\n  }\n\n  stopEditing() {\n    this.setState({editing: false});\n  }\n\n  toggleStatus() {\n    const { task } = this.props;\n    this.props.updateTask(task, {completed: !task.completed});\n  }\n\n  renderTitle(task) {\n    return (\n      <div className=\"task-item__title\" tabIndex=\"0\">\n        {task.title}\n      </div>\n    );\n  }\n\n  renderTitleInput(task) {\n    return (\n      <input\n        autoComplete=\"off\"\n        autoFocus\n        className=\"task-item__input\"\n        defaultValue={task.title}\n        maxLength=\"64\"\n        onKeyUp={this.handleKeyUp}\n        type=\"text\"\n      />\n    );\n  }\n\n  render() {\n    const { editing } = this.state;\n    const { task } = this.props;\n\n    let containerClasses = classNames('task-item', {\n      'task-item--completed': task.completed,\n      'task-item--editing': editing\n    });\n\n    return (\n      <div className={containerClasses} tabIndex=\"0\">\n        <div className=\"cell\">\n          <Button\n            className={classNames('btn--icon', 'task-item__button', {'active': task.completed, 'hide': editing})}\n            onClick={this.toggleStatus}>\n            <Icon name=\"done\" />\n          </Button>\n        </div>\n\n        <div className=\"cell\">\n          {editing ? this.renderTitleInput(task) : this.renderTitle(task)}\n        </div>\n\n        <div className=\"cell\">\n          <Button\n            className={classNames('btn--icon', 'task-item__button', {'hide': editing})}\n            onClick={this.edit}>\n            <Icon name=\"mode_edit\" />\n          </Button>\n          <Button\n            className={classNames('btn--icon', 'task-item__button', {'hide': !editing})}\n            onClick={this.stopEditing}>\n            <Icon name=\"clear\" />\n          </Button>\n          <Button\n            className={classNames('btn--icon', 'task-item__button', {'hide': editing})}\n            onClick={this.remove}>\n            <Icon name=\"delete\" />\n          </Button>\n        </div>\n      </div>\n    );\n  }\n}\n\nTaskItem.propTypes = {\n  removeTask: PropTypes.func.isRequired,\n  task: PropTypes.object.isRequired,\n  updateTask: PropTypes.func.isRequired\n};\n\n\nexport default TaskItem;\n"
  },
  {
    "path": "src/views/components/task-item/task-item.scss",
    "content": "@import 'views/styles/shared';\n\n\n.task-item {\n  display: flex;\n  outline: none;\n  border-bottom: 1px dotted #666;\n  height: 60px;\n  overflow: hidden;\n  color: #fff;\n  font-size: rem(18px);\n  font-weight: 300;\n\n  @include media-query(540) {\n    font-size: rem(24px);\n  }\n}\n\n.task-item--editing {\n  border-bottom: 1px dotted #ccc;\n}\n\n\n//=====================================\n//  Cells\n//-------------------------------------\n.cell {\n  &:first-child,\n  &:last-child {\n    display: flex;\n    flex: 0 0 auto;\n    align-items: center;\n  }\n\n  &:first-child {\n    padding-right: 20px;\n  }\n\n  &:nth-child(2) {\n    flex: 1;\n    padding-right: 30px;\n    overflow: hidden;\n  }\n}\n\n\n//=====================================\n//  Buttons\n//-------------------------------------\n.task-item__button {\n  margin-left: 5px;\n  background: #2a2a2a;\n\n  &:first-child {\n    margin: 0;\n  }\n\n  color: #555;\n\n  &:hover {\n    color: #999;\n  }\n\n  &:active {\n    background: #262626;\n  }\n\n  &.active {\n    color: #85bf6b;\n  }\n}\n\n\n//=====================================\n//  Title (static)\n//-------------------------------------\n.task-item__title {\n  display: inline-block;\n  position: relative;\n  max-width: 100%;\n  line-height: 60px;\n  outline: none;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n\n  &:after {\n    position: absolute;\n    left: 0;\n    bottom: 0;\n    border-top: 2px solid #85bf6b;\n    width: 0;\n    height: 46%;\n    content: '';\n  }\n\n  .task-item--completed & {\n    color: #666;\n  }\n\n  .task-item--completed &:after {\n    width: 100%;\n  }\n}\n\n\n//=====================================\n//  Title (input)\n//-------------------------------------\n.task-item__input {\n  outline: none;\n  border: 0;\n  padding: 0;\n  width: 100%;\n  height: 60px;\n  color: inherit;\n  font: inherit;\n  background: transparent;\n\n  // hide `x`\n  &::-ms-clear {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src/views/components/task-list/index.js",
    "content": "export { default } from './task-list';\n"
  },
  {
    "path": "src/views/components/task-list/task-list.js",
    "content": "import React from 'react';\nimport { List } from 'immutable';\nimport PropTypes from 'prop-types';\nimport TaskItem from '../task-item';\n\nimport './task-list.css';\n\n\nconst TaskList = ({removeTask, tasks, updateTask}) => {\n  let taskItems = tasks.map((task, index) => {\n    return (\n      <TaskItem\n        removeTask={removeTask}\n        key={index}\n        task={task}\n        updateTask={updateTask}\n      />\n    );\n  });\n\n  return (\n    <div className=\"task-list\">\n      {taskItems}\n    </div>\n  );\n};\n\nTaskList.propTypes = {\n  removeTask: PropTypes.func.isRequired,\n  tasks: PropTypes.instanceOf(List),\n  updateTask: PropTypes.func.isRequired\n};\n\n\nexport default TaskList;\n"
  },
  {
    "path": "src/views/components/task-list/task-list.scss",
    "content": ".task-list {\n  border-top: 1px dotted #666;\n}\n"
  },
  {
    "path": "src/views/pages/sign-in/index.js",
    "content": "export { default } from './sign-in-page';\n"
  },
  {
    "path": "src/views/pages/sign-in/sign-in-page.js",
    "content": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { withRouter } from 'react-router-dom';\nimport { authActions } from 'src/auth';\nimport Button from 'src/views/components/button';\n\nimport './sign-in-page.css';\n\n\nconst SignInPage = ({signInWithGithub, signInWithGoogle, signInWithTwitter}) => {\n  return (\n    <div className=\"g-row sign-in\">\n      <div className=\"g-col\">\n        <h1 className=\"sign-in__heading\">Sign in</h1>\n        <Button className=\"sign-in__button\" onClick={signInWithGithub}>GitHub</Button>\n        <Button className=\"sign-in__button\" onClick={signInWithGoogle}>Google</Button>\n        <Button className=\"sign-in__button\" onClick={signInWithTwitter}>Twitter</Button>\n      </div>\n    </div>\n  );\n};\n\nSignInPage.propTypes = {\n  signInWithGithub: PropTypes.func.isRequired,\n  signInWithGoogle: PropTypes.func.isRequired,\n  signInWithTwitter: PropTypes.func.isRequired\n};\n\n\n//=====================================\n//  CONNECT\n//-------------------------------------\n\nconst mapDispatchToProps = {\n  signInWithGithub: authActions.signInWithGithub,\n  signInWithGoogle: authActions.signInWithGoogle,\n  signInWithTwitter: authActions.signInWithTwitter\n};\n\nexport default withRouter(\n    connect(\n    null,\n    mapDispatchToProps\n  )(SignInPage)\n);\n"
  },
  {
    "path": "src/views/pages/sign-in/sign-in-page.scss",
    "content": "@import 'views/styles/shared';\n\n\n.sign-in {\n  margin-top: 90px;\n  max-width: 300px;\n}\n\n.sign-in__heading {\n  margin-bottom: 36px;\n  font-size: 30px;\n  font-weight: 300;\n  text-align: center;\n}\n\n.sign-in__button {\n  margin-bottom: 10px;\n  border: 1px solid #555;\n  width: 100%;\n  height: 48px;\n  font-family: inherit;\n  font-size: rem(18px);\n  line-height: 48px;\n  color: #999;\n\n  &:hover {\n    border: 2px solid #aaa;\n    line-height: 46px;\n  }\n}\n"
  },
  {
    "path": "src/views/pages/tasks/index.js",
    "content": "export { default } from './tasks-page';\n"
  },
  {
    "path": "src/views/pages/tasks/tasks-page.js",
    "content": "import React from 'react';\nimport { List } from 'immutable';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { withRouter } from 'react-router-dom';\nimport { taskActions, getVisibleTasks } from 'src/tasks';\nimport TaskFilters from 'src/views/components/task-filters';\nimport TaskForm from 'src/views/components/task-form';\nimport TaskList from 'src/views/components/task-list';\n\n\nconst TasksPage = ({createTask, location, removeTask, tasks, updateTask}) => {\n  const params = new URLSearchParams(location.search);\n  const filter = params.get('filter');\n\n  return (\n    <div className=\"g-row\">\n      <div className=\"g-col\">\n        <TaskForm handleSubmit={createTask} />\n      </div>\n\n      <div className=\"g-col\">\n        <TaskFilters filter={filter} />\n        <TaskList\n          filter={filter}\n          removeTask={removeTask}\n          tasks={tasks}\n          updateTask={updateTask}\n        />\n      </div>\n    </div>\n  );\n};\n\nTasksPage.propTypes = {\n  createTask: PropTypes.func.isRequired,\n  filterTasks: PropTypes.func.isRequired,\n  location: PropTypes.object.isRequired,\n  removeTask: PropTypes.func.isRequired,\n  tasks: PropTypes.instanceOf(List),\n  updateTask: PropTypes.func.isRequired\n};\n\n\n//=====================================\n//  CONNECT\n//-------------------------------------\n\nconst mapStateToProps = state => ({\n  tasks: getVisibleTasks(state)\n});\n\nconst mapDispatchToProps = {\n  createTask: taskActions.createTask,\n  filterTasks: taskActions.filterTasks,\n  removeTask: taskActions.removeTask,\n  updateTask: taskActions.updateTask\n};\n\nexport default withRouter(\n  connect(\n    mapStateToProps,\n    mapDispatchToProps\n  )(TasksPage)\n);\n"
  },
  {
    "path": "src/views/styles/_grid.scss",
    "content": ".g-row {\n  @include grid-row;\n}\n\n.g-col {\n  @include grid-column;\n  width: 100%;\n}\n"
  },
  {
    "path": "src/views/styles/_settings.scss",
    "content": "$base-background-color: #222 !default;\n$base-font-color: #999 !default;\n$base-font-family: 'aktiv-grotesk-std', Helvetica Neue, Arial, sans-serif !default;\n$base-font-size: 18px !default;\n$base-line-height: 24px !default;\n\n\n//=====================================\n//  Grid\n//-------------------------------------\n$grid-max-width: 810px !default;\n"
  },
  {
    "path": "src/views/styles/_shared.scss",
    "content": "@import\n'./settings',\n'minx/src/settings',\n'minx/src/functions',\n'minx/src/mixins';\n"
  },
  {
    "path": "src/views/styles/styles.scss",
    "content": "@import\n'./shared',\n'minx/src/reset',\n'minx/src/elements',\n'./grid';\n\n\nhtml {\n  overflow-y: scroll;\n}\n\nbody {\n  padding-bottom: 120px;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n::selection {\n  background: rgba(200,200,255,.1);\n}\n\n.hide {\n  display: none !important;\n}\n"
  }
]