Full Code of r-park/todo-redux-saga for AI

master d693f0f757f0 cached
72 files
47.3 KB
14.2k tokens
42 symbols
1 requests
Download .txt
Repository: r-park/todo-redux-saga
Branch: master
Commit: d693f0f757f0
Files: 72
Total size: 47.3 KB

Directory structure:
gitextract_if41cgni/

├── .firebaserc
├── .gitignore
├── LICENSE
├── README.md
├── circle.yml
├── firebase.json
├── firebase.rules.json
├── package.json
├── public/
│   ├── index.html
│   └── manifest.json
└── src/
    ├── auth/
    │   ├── actions.js
    │   ├── auth.js
    │   ├── index.js
    │   ├── reducer.js
    │   ├── sagas.js
    │   └── selectors.js
    ├── firebase/
    │   ├── config.js
    │   ├── firebase-list.js
    │   ├── firebase.js
    │   └── index.js
    ├── history.js
    ├── index.js
    ├── reducers.js
    ├── register-service-worker.js
    ├── sagas.js
    ├── store.js
    ├── tasks/
    │   ├── actions.js
    │   ├── index.js
    │   ├── reducer.js
    │   ├── sagas.js
    │   ├── selectors.js
    │   ├── task-list.js
    │   └── task.js
    └── views/
        ├── app/
        │   ├── app.js
        │   └── index.js
        ├── components/
        │   ├── button/
        │   │   ├── button.js
        │   │   ├── button.scss
        │   │   ├── button.spec.js
        │   │   └── index.js
        │   ├── github-logo/
        │   │   ├── github-logo.js
        │   │   └── index.js
        │   ├── header/
        │   │   ├── header.js
        │   │   ├── header.scss
        │   │   └── index.js
        │   ├── icon/
        │   │   ├── icon.js
        │   │   ├── icon.spec.js
        │   │   └── index.js
        │   ├── require-auth-route/
        │   │   ├── index.js
        │   │   └── require-auth-route.js
        │   ├── require-unauth-route/
        │   │   ├── index.js
        │   │   └── require-unauth-route.js
        │   ├── task-filters/
        │   │   ├── index.js
        │   │   ├── task-filters.js
        │   │   └── task-filters.scss
        │   ├── task-form/
        │   │   ├── index.js
        │   │   ├── task-form.js
        │   │   └── task-form.scss
        │   ├── task-item/
        │   │   ├── index.js
        │   │   ├── task-item.js
        │   │   └── task-item.scss
        │   └── task-list/
        │       ├── index.js
        │       ├── task-list.js
        │       └── task-list.scss
        ├── pages/
        │   ├── sign-in/
        │   │   ├── index.js
        │   │   ├── sign-in-page.js
        │   │   └── sign-in-page.scss
        │   └── tasks/
        │       ├── index.js
        │       └── tasks-page.js
        └── styles/
            ├── _grid.scss
            ├── _settings.scss
            ├── _shared.scss
            └── styles.scss

================================================
FILE CONTENTS
================================================

================================================
FILE: .firebaserc
================================================
{
  "projects": {
    "default": "todo-redux-saga"
  }
}


================================================
FILE: .gitignore
================================================
#======================================
# Directories
#--------------------------------------
build/
dist/
coverage/
node_modules/
tmp/


#======================================
# Extensions
#--------------------------------------
*.css
*.gz
*.local
*.log
*.rar
*.tar
*.zip


#======================================
# IDE generated
#--------------------------------------
.idea/
.project
*.iml


#======================================
# OS generated
#--------------------------------------
__MACOSX/
.DS_Store
Thumbs.db


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2016 Richard Park (objectiv@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
[![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)


# 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

Try the demo at <a href="https://todo-redux-saga.firebaseapp.com" target="_blank">todo-redux-saga.firebaseapp.com</a>.


## Stack

- Create React App
- React Redux
- React Router
- React Router Redux
- Redux Saga
- Redux Devtools Extension for Chrome
- Firebase SDK with OAuth authentication
- Immutable
- Reselect
- SASS


## Quick Start

```shell
$ git clone https://github.com/r-park/todo-redux-saga.git
$ cd todo-redux-saga
$ npm install
$ npm start
```

## Deploying to Firebase
#### Prerequisites:
- Create a free Firebase account at https://firebase.google.com
- Create a project from your [Firebase account console](https://console.firebase.google.com)
- Configure the authentication providers for your Firebase project from your Firebase account console

#### Configure this app with your project-specific details:
```json
// .firebaserc

{
  "projects": {
    "default": "your-project-id"
  }
}
```

```javascript
// src/firebase/config.js

export const firebaseConfig = {
  apiKey: 'your api key',
  authDomain: 'your-project-id.firebaseapp.com',
  databaseURL: 'https://your-project-id.firebaseio.com',
  storageBucket: 'your-project-id.appspot.com'
};
```

#### Install firebase-tools:
```shell
$ npm install -g firebase-tools
```

#### Build and deploy the app:
```shell
$ npm run build
$ firebase login
$ firebase use default
$ firebase deploy
```


## NPM Commands

|Script|Description|
|---|---|
|`npm start`|Start webpack development server @ `localhost:3000`|
|`npm run build`|Build the application to `./build` directory|
|`npm test`|Test the application; watch for changes and retest|


================================================
FILE: circle.yml
================================================
machine:
  node:
    version: 8.1

dependencies:
  pre:
    - rm -rf node_modules

test:
  override:
    - npm run build
    - npm test

deployment:
  production:
    branch: master
    commands:
      - ./node_modules/.bin/firebase deploy --token $FIREBASE_TOKEN


================================================
FILE: firebase.json
================================================
{
  "database": {
    "rules": "firebase.rules.json"
  },

  "hosting": {
    "public": "build",
    "headers": [
      {
        "source": "**/*",
        "headers": [
          {"key": "X-Content-Type-Options", "value": "nosniff"},
          {"key": "X-Frame-Options", "value": "DENY"},
          {"key": "X-UA-Compatible", "value": "ie=edge"},
          {"key": "X-XSS-Protection", "value": "1; mode=block"}
        ]
      },
      {
        "source": "**/*.@(css|html|js|map)",
        "headers": [
          {"key": "Cache-Control", "value": "max-age=3600"}
        ]
      }
    ],
    "rewrites": [
      {"source": "**", "destination": "/index.html"}
    ]
  }
}


================================================
FILE: firebase.rules.json
================================================
{
  "rules": {
    "tasks": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid",
        ".write": "auth !== null && auth.uid === $uid"
      }
    }
  }
}


================================================
FILE: package.json
================================================
{
  "name": "todo-redux-saga",
  "version": "0.0.0",
  "description": "Todo app with React, Redux, Redux-Saga, and Firebase",
  "homepage": "https://todo-redux-saga.firebaseapp.com",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/r-park/todo-redux-saga.git"
  },
  "author": {
    "name": "Richard Park",
    "email": "objectiv@gmail.com"
  },
  "license": "MIT",
  "private": true,
  "engines": {
    "node": ">=8.1.4"
  },
  "scripts": {
    "eject": "react-scripts eject",
    "build": "run-s build.css build.js",
    "build.css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/",
    "build.js": "cross-env NODE_PATH=. react-scripts build",
    "start": "run-p start.css start.js",
    "start.css": "npm run build.css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive",
    "start.js": "cross-env NODE_PATH=. react-scripts start",
    "test": "cross-env NODE_PATH=. react-scripts test --env=jsdom",
    "test.ci": "cross-env CI=true NODE_PATH=. react-scripts test --env=jsdom"
  },
  "dependencies": {
    "classnames": "^2.2.5",
    "firebase": "^4.1.3",
    "history": "^4.6.3",
    "immutable": "^3.8.1",
    "prop-types": "^15.5.10",
    "react": "^15.6.1",
    "react-dom": "^15.6.1",
    "react-redux": "^5.0.5",
    "react-router": "^4.1.1",
    "react-router-dom": "^4.1.1",
    "react-router-redux": "^5.0.0-alpha.6",
    "react-scripts": "1.0.10",
    "redux": "^3.7.1",
    "redux-saga": "^0.15.4",
    "reselect": "^3.0.1"
  },
  "devDependencies": {
    "cross-env": "^5.0.1",
    "enzyme": "^2.9.1",
    "firebase-tools": "^3.9.1",
    "minx": "r-park/minx.git",
    "node-sass-chokidar": "0.0.3",
    "npm-run-all": "^4.0.2",
    "react-test-renderer": "^15.6.1"
  }
}


================================================
FILE: public/index.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">

    <title>Todo Redux Saga</title>

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">

    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">

    <script src="https://use.typekit.net/kmv5qdr.js"></script>
    <script>try{Typekit.load({ async: true });}catch(e){}</script>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>


================================================
FILE: public/manifest.json
================================================
{
  "short_name": "Todo Redux Saga",
  "name": "Todo app with React, Redux, Redux-Saga, and Firebase",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "start_url": "./index.html",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}


================================================
FILE: src/auth/actions.js
================================================
import firebase from 'firebase/app';


export const authActions = {
  SIGN_IN: 'SIGN_IN',
  SIGN_IN_FAILED: 'SIGN_IN_FAILED',
  SIGN_IN_FULFILLED: 'SIGN_IN_FULFILLED',

  SIGN_OUT: 'SIGN_OUT',
  SIGN_OUT_FAILED: 'SIGN_OUT_FAILED',
  SIGN_OUT_FULFILLED: 'SIGN_OUT_FULFILLED',


  signIn: authProvider => ({
    type: authActions.SIGN_IN,
    payload: {authProvider}
  }),

  signInFailed: error => ({
    type: authActions.SIGN_IN_FAILED,
    payload: {error}
  }),

  signInFulfilled: authUser => ({
    type: authActions.SIGN_IN_FULFILLED,
    payload: {authUser}
  }),

  signInWithGithub: () => authActions.signIn(
    new firebase.auth.GithubAuthProvider()
  ),

  signInWithGoogle: () => authActions.signIn(
    new firebase.auth.GoogleAuthProvider()
  ),

  signInWithTwitter: () => authActions.signIn(
    new firebase.auth.TwitterAuthProvider()
  ),

  signOut: () => ({
    type: authActions.SIGN_OUT
  }),

  signOutFailed: error => ({
    type: authActions.SIGN_OUT_FAILED,
    payload: {error}
  }),

  signOutFulfilled: () => ({
    type: authActions.SIGN_OUT_FULFILLED
  })
};


================================================
FILE: src/auth/auth.js
================================================
import { firebaseAuth } from 'src/firebase';
import { authActions } from './actions';


export function initAuth(dispatch) {
  return new Promise((resolve, reject) => {
    const unsubscribe = firebaseAuth.onAuthStateChanged(
      authUser => {
        if (authUser) {
          dispatch(authActions.signInFulfilled(authUser));
        }

        resolve();
        unsubscribe();
      },

      error => reject(error)
    );
  });
}


================================================
FILE: src/auth/index.js
================================================
export { authActions } from './actions';
export { initAuth } from './auth';
export { authReducer } from './reducer';
export { authSagas } from './sagas';
export { getAuth, isAuthenticated } from './selectors';


================================================
FILE: src/auth/reducer.js
================================================
import { Record } from 'immutable';
import { authActions } from './actions';


export const AuthState = new Record({
  authenticated: false,
  uid: null,
  user: null
});


export function authReducer(state = new AuthState(), {payload, type}) {
  switch (type) {
    case authActions.SIGN_IN_FULFILLED:
      return state.merge({
        authenticated: true,
        uid: payload.uid,
        user: payload
      });

    case authActions.SIGN_OUT_FULFILLED:
      return state.merge({
        authenticated: false,
        uid: null,
        user: null
      });

    default:
      return state;
  }
}


================================================
FILE: src/auth/sagas.js
================================================
import { call, fork, put, take } from 'redux-saga/effects';
import { firebaseAuth } from 'src/firebase';
import history from 'src/history';
import { authActions } from './actions';


function* signIn(authProvider) {
  try {
    const authData = yield call([firebaseAuth, firebaseAuth.signInWithPopup], authProvider);
    yield put(authActions.signInFulfilled(authData.user));
    yield history.push('/');
  }
  catch (error) {
    yield put(authActions.signInFailed(error));
  }
}

function* signOut() {
  try {
    yield call([firebaseAuth, firebaseAuth.signOut]);
    yield put(authActions.signOutFulfilled());
    yield history.replace('/sign-in');
  }
  catch (error) {
    yield put(authActions.signOutFailed(error));
  }
}


//=====================================
//  WATCHERS
//-------------------------------------

function* watchSignIn() {
  while (true) {
    let { payload } = yield take(authActions.SIGN_IN);
    yield fork(signIn, payload.authProvider);
  }
}

function* watchSignOut() {
  while (true) {
    yield take(authActions.SIGN_OUT);
    yield fork(signOut);
  }
}


//=====================================
//  AUTH SAGAS
//-------------------------------------

export const authSagas = [
  fork(watchSignIn),
  fork(watchSignOut)
];


================================================
FILE: src/auth/selectors.js
================================================
import { createSelector } from 'reselect';


export function isAuthenticated(state) {
  return state.auth.authenticated;
}


//=====================================
//  MEMOIZED SELECTORS
//-------------------------------------

export const getAuth = createSelector(
  state => state.auth,
  auth => auth.toJS()
);


================================================
FILE: src/firebase/config.js
================================================
export const firebaseConfig = {
  apiKey: 'AIzaSyCUll5AyYba1XL8NDYKZ51RGt90KofQo6c',
  authDomain: 'todo-redux-saga.firebaseapp.com',
  databaseURL: 'https://todo-redux-saga.firebaseio.com',
  storageBucket: 'todo-redux-saga.appspot.com'
};


================================================
FILE: src/firebase/firebase-list.js
================================================
import { firebaseDb } from './firebase';


export class FirebaseList {
  constructor(actions, modelClass) {
    this._actions = actions;
    this._modelClass = modelClass;
  }

  get path() {
    return this._path;
  }

  set path(value) {
    this._path = value;
  }

  push(value) {
    return new Promise((resolve, reject) => {
      firebaseDb.ref(this.path)
        .push(value, error => error ? reject(error) : resolve());
    });
  }

  remove(key) {
    return new Promise((resolve, reject) => {
      firebaseDb.ref(`${this.path}/${key}`)
        .remove(error => error ? reject(error) : resolve());
    });
  }

  update(key, value) {
    return new Promise((resolve, reject) => {
      firebaseDb.ref(`${this.path}/${key}`)
        .update(value, error => error ? reject(error) : resolve());
    });
  }

  subscribe(emit) {
    let ref = firebaseDb.ref(this.path);
    let initialized = false;
    let list = [];

    ref.once('value', () => {
      initialized = true;
      emit(this._actions.onLoad(list));
    });

    ref.on('child_added', snapshot => {
      if (initialized) {
        emit(this._actions.onAdd(this.unwrapSnapshot(snapshot)));
      }
      else {
        list.push(this.unwrapSnapshot(snapshot));
      }
    });

    ref.on('child_changed', snapshot => {
      emit(this._actions.onChange(this.unwrapSnapshot(snapshot)));
    });

    ref.on('child_removed', snapshot => {
      emit(this._actions.onRemove(this.unwrapSnapshot(snapshot)));
    });

    return () => ref.off();
  }

  unwrapSnapshot(snapshot) {
    let attrs = snapshot.val();
    attrs.key = snapshot.key;
    return new this._modelClass(attrs);
  }
}


================================================
FILE: src/firebase/firebase.js
================================================
import firebase from 'firebase/app';

import 'firebase/auth';
import 'firebase/database';

import { firebaseConfig } from './config';


export const firebaseApp = firebase.initializeApp(firebaseConfig);
export const firebaseAuth = firebase.auth();
export const firebaseDb = firebase.database();


================================================
FILE: src/firebase/index.js
================================================
export { firebaseApp, firebaseAuth, firebaseDb } from './firebase';
export { FirebaseList } from './firebase-list';


================================================
FILE: src/history.js
================================================
import createHistory from 'history/createBrowserHistory';


export default createHistory();


================================================
FILE: src/index.js
================================================
import './views/styles/styles.css';

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';

import { initAuth } from './auth';
import history from './history';
import configureStore from './store';
import App from './views/app';
import registerServiceWorker from './register-service-worker';


const store = configureStore();
const rootElement = document.getElementById('root');


function render(Component) {
  ReactDOM.render(
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <div>
          <Component/>
        </div>
      </ConnectedRouter>
    </Provider>,
    rootElement
  );
}


if (module.hot) {
  module.hot.accept('./views/app', () => {
    render(require('./views/app').default);
  })
}


registerServiceWorker();


initAuth(store.dispatch)
  .then(() => render(App))
  .catch(error => console.error(error));


================================================
FILE: src/reducers.js
================================================
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { authReducer } from './auth';
import { tasksReducer } from './tasks';


export default combineReducers({
  auth: authReducer,
  routing: routerReducer,
  tasks: tasksReducer
});


================================================
FILE: src/register-service-worker.js
================================================
// In production, we register a service worker to serve assets from local cache.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.

// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    // [::1] is the IPv6 localhost address.
    window.location.hostname === '[::1]' ||
    // 127.0.0.1/8 is considered localhost for IPv4.
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
    )
);

export default function register() {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (!isLocalhost) {
        // Is not local host. Just register service worker
        registerValidSW(swUrl);
      } else {
        // This is running on localhost. Lets check if a service worker still exists or not.
        checkValidServiceWorker(swUrl);
      }
    });
  }
}

function registerValidSW(swUrl) {
  navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
      registration.onupdatefound = () => {
        const installingWorker = registration.installing;
        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              // At this point, the old content will have been purged and
              // the fresh content will have been added to the cache.
              // It's the perfect time to display a "New content is
              // available; please refresh." message in your web app.
              console.log('New content is available; please refresh.');
            } else {
              // At this point, everything has been precached.
              // It's the perfect time to display a
              // "Content is cached for offline use." message.
              console.log('Content is cached for offline use.');
            }
          }
        };
      };
    })
    .catch(error => {
      console.error('Error during service worker registration:', error);
    });
}

function checkValidServiceWorker(swUrl) {
  // Check if the service worker can be found. If it can't reload the page.
  fetch(swUrl)
    .then(response => {
      // Ensure service worker exists, and that we really are getting a JS file.
      if (
        response.status === 404 ||
        response.headers.get('content-type').indexOf('javascript') === -1
      ) {
        // No service worker found. Probably a different app. Reload the page.
        navigator.serviceWorker.ready.then(registration => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        // Service worker found. Proceed as normal.
        registerValidSW(swUrl);
      }
    })
    .catch(() => {
      console.log(
        'No internet connection found. App is running in offline mode.'
      );
    });
}

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(registration => {
      registration.unregister();
    });
  }
}


================================================
FILE: src/sagas.js
================================================
import { all } from 'redux-saga/effects'
import { authSagas } from './auth';
import { taskSagas } from './tasks';


export default function* sagas() {
  yield all([
    ...authSagas,
    ...taskSagas
  ]);
}


================================================
FILE: src/store.js
================================================
import { routerMiddleware } from 'react-router-redux';
import { applyMiddleware, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import history from './history';
import reducers from './reducers';
import sagas from './sagas';


export default function configureStore() {
  const sagaMiddleware = createSagaMiddleware();
  let middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history));

  if (process.env.NODE_ENV !== 'production') {
    const devToolsExtension = window.devToolsExtension;
    if (typeof devToolsExtension === 'function') {
      middleware = compose(middleware, devToolsExtension());
    }
  }

  const store = createStore(reducers, middleware);
  sagaMiddleware.run(sagas);

  if (module.hot) {
    module.hot.accept('./reducers', () => {
      store.replaceReducer(require('./reducers').default);
    });
  }

  return store;
}


================================================
FILE: src/tasks/actions.js
================================================
export const taskActions = {
  CREATE_TASK: 'CREATE_TASK',
  CREATE_TASK_FAILED: 'CREATE_TASK_FAILED',
  CREATE_TASK_FULFILLED: 'CREATE_TASK_FULFILLED',

  REMOVE_TASK: 'REMOVE_TASK',
  REMOVE_TASK_FAILED: 'REMOVE_TASK_FAILED',
  REMOVE_TASK_FULFILLED: 'REMOVE_TASK_FULFILLED',

  UPDATE_TASK: 'UPDATE_TASK',
  UPDATE_TASK_FAILED: 'UPDATE_TASK_FAILED',
  UPDATE_TASK_FULFILLED: 'UPDATE_TASK_FULFILLED',

  FILTER_TASKS: 'FILTER_TASKS',
  LOAD_TASKS_FULFILLED: 'LOAD_TASKS_FULFILLED',


  createTask: title => ({
    type: taskActions.CREATE_TASK,
    payload: {task: {title, completed: false}}
  }),

  createTaskFailed: error => ({
    type: taskActions.CREATE_TASK_FAILED,
    payload: {error}
  }),

  createTaskFulfilled: task => ({
    type: taskActions.CREATE_TASK_FULFILLED,
    payload: {task}
  }),

  removeTask: task => ({
    type: taskActions.REMOVE_TASK,
    payload: {task}
  }),

  removeTaskFailed: error => ({
    type: taskActions.REMOVE_TASK_FAILED,
    payload: {error}
  }),

  removeTaskFulfilled: task => ({
    type: taskActions.REMOVE_TASK_FULFILLED,
    payload: {task}
  }),

  updateTask: (task, changes) => ({
    type: taskActions.UPDATE_TASK,
    payload: {task, changes}
  }),

  updateTaskFailed: error => ({
    type: taskActions.UPDATE_TASK_FAILED,
    payload: {error}
  }),

  updateTaskFulfilled: task => ({
    type: taskActions.UPDATE_TASK_FULFILLED,
    payload: {task}
  }),

  filterTasks: filterType => ({
    type: taskActions.FILTER_TASKS,
    payload: {filterType}
  }),

  loadTasksFulfilled: tasks => ({
    type: taskActions.LOAD_TASKS_FULFILLED,
    payload: {tasks}
  })
};


================================================
FILE: src/tasks/index.js
================================================
export { taskActions } from './actions';
export { tasksReducer } from './reducer';
export { taskSagas } from './sagas';
export { getVisibleTasks } from './selectors';


================================================
FILE: src/tasks/reducer.js
================================================
import { List, Record } from 'immutable';
import { taskActions } from './actions';


export const TasksState = new Record({
  filter: '',
  list: new List()
});


export function tasksReducer(state = new TasksState(), {payload, type}) {
  switch (type) {
    case taskActions.CREATE_TASK_FULFILLED:
      return state.set('list', state.list.unshift(payload.task));

    case taskActions.FILTER_TASKS:
      return state.set('filter', payload.filterType || '');

    case taskActions.LOAD_TASKS_FULFILLED:
      return state.set('list', new List(payload.tasks.reverse()));

    case taskActions.REMOVE_TASK_FULFILLED:
      return state.set('list', state.list.filter(task => {
        return task.key !== payload.task.key;
      }));

    case taskActions.UPDATE_TASK_FULFILLED:
      return state.set('list', state.list.map(task => {
        return task.key === payload.task.key ? payload.task : task;
      }));

    default:
      return state;
  }
}


================================================
FILE: src/tasks/sagas.js
================================================
import { LOCATION_CHANGE } from 'react-router-redux';
import { eventChannel } from 'redux-saga';
import { call, cancel, fork, put, take } from 'redux-saga/effects';
import { authActions } from 'src/auth';
import { taskActions } from './actions';
import { taskList } from './task-list';


function subscribe() {
  return eventChannel(emit => taskList.subscribe(emit));
}

function* read() {
  const channel = yield call(subscribe);
  while (true) {
    let action = yield take(channel);
    yield put(action);
  }
}

function* write(context, method, onError, ...params) {
  try {
    yield call([context, method], ...params);
  }
  catch (error) {
    yield put(onError(error));
  }
}

const createTask = write.bind(null, taskList, taskList.push, taskActions.createTaskFailed);
const removeTask = write.bind(null, taskList, taskList.remove, taskActions.removeTaskFailed);
const updateTask = write.bind(null, taskList, taskList.update, taskActions.updateTaskFailed);


//=====================================
//  WATCHERS
//-------------------------------------

function* watchAuthentication() {
  while (true) {
    let { payload } = yield take(authActions.SIGN_IN_FULFILLED);

    taskList.path = `tasks/${payload.authUser.uid}`;
    const job = yield fork(read);

    yield take([authActions.SIGN_OUT_FULFILLED]);
    yield cancel(job);
  }
}

function* watchCreateTask() {
  while (true) {
    let { payload } = yield take(taskActions.CREATE_TASK);
    yield fork(createTask, payload.task);
  }
}

function* watchLocationChange() {
  while (true) {
    let { payload } = yield take(LOCATION_CHANGE);
    if (payload.pathname === '/') {
      const params = new URLSearchParams(payload.search);
      const filter = params.get('filter');
      yield put(taskActions.filterTasks(filter));
    }
  }
}

function* watchRemoveTask() {
  while (true) {
    let { payload } = yield take(taskActions.REMOVE_TASK);
    yield fork(removeTask, payload.task.key);
  }
}

function* watchUpdateTask() {
  while (true) {
    let { payload } = yield take(taskActions.UPDATE_TASK);
    yield fork(updateTask, payload.task.key, payload.changes);
  }
}


//=====================================
//  TASK SAGAS
//-------------------------------------

export const taskSagas = [
  fork(watchAuthentication),
  fork(watchCreateTask),
  fork(watchLocationChange),
  fork(watchRemoveTask),
  fork(watchUpdateTask)
];


================================================
FILE: src/tasks/selectors.js
================================================
import { createSelector } from 'reselect';


export function getTasks(state) {
  return state.tasks;
}

export function getTaskFilter(state) {
  return getTasks(state).filter;
}

export function getTaskList(state) {
  return getTasks(state).list;
}


//=====================================
//  MEMOIZED SELECTORS
//-------------------------------------

export const getVisibleTasks = createSelector(
  getTaskFilter,
  getTaskList,
  (filter, taskList) => {
    switch (filter) {
      case 'active':
        return taskList.filter(task => !task.completed);

      case 'completed':
        return taskList.filter(task => task.completed);

      default:
        return taskList;
    }
  }
);


================================================
FILE: src/tasks/task-list.js
================================================
import { FirebaseList } from 'src/firebase';
import { taskActions } from './actions';
import { Task } from './task';


export const taskList = new FirebaseList({
  onAdd: taskActions.createTaskFulfilled,
  onChange: taskActions.updateTaskFulfilled,
  onLoad: taskActions.loadTasksFulfilled,
  onRemove: taskActions.removeTaskFulfilled
}, Task);


================================================
FILE: src/tasks/task.js
================================================
import { Record } from 'immutable';


export const Task = new Record({
  completed: false,
  key: null,
  title: null
});


================================================
FILE: src/views/app/app.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';

import { authActions, getAuth } from 'src/auth';
import Header from '../components/header';
import RequireAuthRoute from '../components/require-auth-route';
import RequireUnauthRoute from '../components/require-unauth-route';
import SignInPage from '../pages/sign-in';
import TasksPage from '../pages/tasks';


const App = ({authenticated, signOut}) => (
  <div>
    <Header
      authenticated={authenticated}
      signOut={signOut}
    />

    <main>
      <RequireAuthRoute authenticated={authenticated} exact path="/" component={TasksPage}/>
      <RequireUnauthRoute authenticated={authenticated} path="/sign-in" component={SignInPage}/>
    </main>
  </div>
);

App.propTypes = {
  authenticated: PropTypes.bool.isRequired,
  signOut: PropTypes.func.isRequired
};


//=====================================
//  CONNECT
//-------------------------------------

const mapStateToProps = getAuth;

const mapDispatchToProps = {
  signOut: authActions.signOut
};

export default withRouter(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(App)
);


================================================
FILE: src/views/app/index.js
================================================
export { default } from './app';


================================================
FILE: src/views/components/button/button.js
================================================
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';

import './button.css';


const Button = ({children, className, onClick, type = 'button'}) => {
  const cssClasses = classNames('btn', className);
  return (
    <button className={cssClasses} onClick={onClick} type={type}>
      {children}
    </button>
  );
};

Button.propTypes = {
  children: PropTypes.node,
  className: PropTypes.string,
  onClick: PropTypes.func,
  type: PropTypes.oneOf(['button', 'reset', 'submit'])
};


export default Button;


================================================
FILE: src/views/components/button/button.scss
================================================
@import 'views/styles/shared';


.btn {
  @include button-base;
  outline: none;
  border: 0;
  padding: 0;
  overflow: hidden;
  transform: translate(0, 0);
  background: transparent;
}

.btn--icon {
  border-radius: 40px;
  padding: 8px;
  width: 40px;
  height: 40px;
}


================================================
FILE: src/views/components/button/button.spec.js
================================================
import React from 'react';
import { render, shallow } from 'enzyme';
import Button from './button';


describe('Button', () => {
  it('should render a button with text node', () => {
    const wrapper = render(<Button>Foo</Button>);
    const button = wrapper.find('button');

    expect(button.length).toBe(1);
    expect(button.text()).toBe('Foo');
  });

  it('should render a button with child element', () => {
    const wrapper = shallow(<Button><span>Foo</span></Button>);
    const button = wrapper.find('button');

    expect(button.length).toBe(1);
    expect(button.contains(<span>Foo</span>)).toBe(true);
  });

  it('should set default className', () => {
    const wrapper = render(<Button />);
    const button = wrapper.find('button');

    expect(button.hasClass('btn')).toBe(true);
  });

  it('should add provided props.className', () => {
    const wrapper = render(<Button className="foo bar" />);
    const button = wrapper.find('button');

    expect(button.hasClass('btn foo bar')).toBe(true);
  });

  it('should set type=button by default', () => {
    const wrapper = render(<Button />);
    const button = wrapper.find('button');

    expect(button.attr('type')).toBe('button');
  });

  it('should set type with provided props.type', () => {
    const wrapper = render(<Button type="submit" />);
    const button = wrapper.find('button');

    expect(button.attr('type')).toBe('submit');
  });

  it('should set onClick with provided props.onClick', () => {
    const handleClick = jasmine.createSpy('handleClick');
    const wrapper = shallow(<Button onClick={handleClick} />);

    wrapper.simulate('click');

    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});


================================================
FILE: src/views/components/button/index.js
================================================
export { default } from './button';


================================================
FILE: src/views/components/github-logo/github-logo.js
================================================
import React from 'react';


export default function GitHubLogo() {
  return (
    <svg viewBox="0 0 20 20">
      <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" />
    </svg>
  );
}


================================================
FILE: src/views/components/github-logo/index.js
================================================
export { default } from './github-logo';


================================================
FILE: src/views/components/header/header.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Button from '../button';
import GitHubLogo from '../github-logo';

import './header.css';


const Header = ({authenticated, signOut}) => (
  <header className="header">
    <div className="g-row">
      <div className="g-col">
        <h1 className="header__title">Todo Redux Saga</h1>

        <ul className="header__actions">
          {authenticated ? <li><Button onClick={signOut}>Sign out</Button></li> : null}
          <li>
            <a className="link link--github" href="https://github.com/r-park/todo-redux-saga">
              <GitHubLogo />
            </a>
          </li>
        </ul>
      </div>
    </div>
  </header>
);

Header.propTypes = {
  authenticated: PropTypes.bool.isRequired,
  signOut: PropTypes.func.isRequired
};


export default Header;


================================================
FILE: src/views/components/header/header.scss
================================================
@import 'views/styles/shared';


.header {
  padding: 10px 0;
  height: 60px;
  overflow: hidden;
  line-height: 40px;
}

.header__title {
  float: left;
  font-size: rem(14px);
  font-weight: 400;
  text-rendering: auto;
  transform: translate(0,0);

  &:before {
    padding-right: 5px;
    color: #fff;
    line-height: 20px;
  }
}

.header__actions {
  @include clearfix;
  float: right;
  padding: 8px 0;
  line-height: 24px;

  li {
    float: left;
    list-style: none;

    &:last-child {
      margin-left: 12px;
      padding-left: 12px;
      border-left: 1px solid #333;
    }

    &:first-child {
      border: none;
    }
  }

  .btn {
    display: block;
    margin: 0;
    color: #999;
    font-size: rem(14px);
    line-height: 24px;
  }

  .link {
    display: block;
    fill: #98999a;
    transform: translate(0, 0);
  }

  .link--github {
    padding-top: 1px;
    width: 22px;
    height: 24px;
  }
}


================================================
FILE: src/views/components/header/index.js
================================================
export { default } from './header';


================================================
FILE: src/views/components/icon/icon.js
================================================
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';


const Icon = ({className, name}) => {
  const cssClasses = classNames('material-icons', className);
  return <span className={cssClasses}>{name}</span>;
};

Icon.propTypes = {
  className: PropTypes.string,
  name: PropTypes.string.isRequired
};


export default Icon;


================================================
FILE: src/views/components/icon/icon.spec.js
================================================
import React from 'react';
import { render, shallow } from 'enzyme';
import Icon from './icon';


describe('Icon', () => {
  it('should render an icon', () => {
    const wrapper = shallow(<Icon name="play" />);
    expect(wrapper.contains(<span className="material-icons">play</span>)).toBe(true);
  });

  it('should add provided props.className', () => {
    const wrapper = render(<Icon className="foo bar" name="play" />);
    const icon = wrapper.find('span');

    expect(icon.hasClass('material-icons foo bar')).toBe(true);
  });
});


================================================
FILE: src/views/components/icon/index.js
================================================
export { default } from './icon';


================================================
FILE: src/views/components/require-auth-route/index.js
================================================
export { default } from './require-auth-route';


================================================
FILE: src/views/components/require-auth-route/require-auth-route.js
================================================
import React from 'react';
import { Route, Redirect } from 'react-router-dom'


const RequireAuthRoute = ({component: Component, authenticated, ...rest}) => (
  <Route
    {...rest}
    render={props => {
      return authenticated ? (
        <Component {...props}/>
      ) : (
        <Redirect to={{
          pathname: '/sign-in',
          state: {from: props.location}
        }}/>
      )
    }}
  />
);


export default RequireAuthRoute;


================================================
FILE: src/views/components/require-unauth-route/index.js
================================================
export { default } from './require-unauth-route';


================================================
FILE: src/views/components/require-unauth-route/require-unauth-route.js
================================================
import React from 'react';
import { Route, Redirect } from 'react-router-dom'


const RequireUnauthRoute = ({component: Component, authenticated, ...rest}) => (
  <Route
    {...rest}
    render={props => {
      return authenticated ? (
        <Redirect to={{
          pathname: '/',
          state: {from: props.location}
        }}/>
      ) : (
        <Component {...props}/>
      )
    }}
  />
);


export default RequireUnauthRoute;


================================================
FILE: src/views/components/task-filters/index.js
================================================
export { default } from './task-filters';


================================================
FILE: src/views/components/task-filters/task-filters.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';

import './task-filters.css';


const TaskFilters = ({filter}) => (
  <ul className="task-filters">
    <li><NavLink isActive={() => !filter} to="/">View All</NavLink></li>
    <li><NavLink isActive={() => filter === 'active'} to={{pathname: '/', search: '?filter=active'}}>Active</NavLink></li>
    <li><NavLink isActive={() => filter === 'completed'} to={{pathname: '/', search: '?filter=completed'}}>Completed</NavLink></li>
  </ul>
);

TaskFilters.propTypes = {
  filter: PropTypes.string
};


export default TaskFilters;


================================================
FILE: src/views/components/task-filters/task-filters.scss
================================================
@import 'views/styles/shared';


.task-filters {
  @include clearfix;
  margin-bottom: 45px;
  padding-left: 1px;
  font-size: rem(16px);
  line-height: 24px;
  list-style-type: none;

  @include media-query(540) {
    margin-bottom: 55px;
  }

  li {
    float: left;

    &:not(:first-child) {
      margin-left: 12px;
    }

    &:not(:first-child):before {
      padding-right: 12px;
      content: '/';
      font-weight: 300;
    }
  }

  a {
    color: #999;
    text-decoration: none;

    &.active {
      color: #fff;
    }
  }
}


================================================
FILE: src/views/components/task-form/index.js
================================================
export { default } from './task-form';


================================================
FILE: src/views/components/task-form/task-form.js
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import './task-form.css';


export class TaskForm extends Component {
  static propTypes = {
    handleSubmit: PropTypes.func.isRequired
  };

  constructor() {
    super(...arguments);

    this.state = {title: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleKeyUp = this.handleKeyUp.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  clearInput() {
    this.setState({title: ''});
  }

  handleChange(event) {
    this.setState({title: event.target.value});
  }

  handleKeyUp(event) {
    if (event.keyCode === 27) this.clearInput();
  }

  handleSubmit(event) {
    event.preventDefault();
    const title = this.state.title.trim();
    if (title.length) this.props.handleSubmit(title);
    this.clearInput();
  }

  render() {
    return (
      <form className="task-form" onSubmit={this.handleSubmit} noValidate>
        <input
          autoComplete="off"
          autoFocus
          className="task-form__input"
          maxLength="64"
          onChange={this.handleChange}
          onKeyUp={this.handleKeyUp}
          placeholder="What needs to be done?"
          type="text"
          value={this.state.title}
        />
      </form>
    );
  }
}


export default TaskForm;


================================================
FILE: src/views/components/task-form/task-form.scss
================================================
@import 'views/styles/shared';


.task-form {
  margin: 40px 0 10px;

  @include media-query(540) {
    margin: 80px 0 20px;
  }
}

.task-form__input {
  outline: none;
  border: 0;
  border-bottom: 1px dotted #666;
  border-radius: 0;
  padding: 0 0 5px 0;
  width: 100%;
  height: 50px;
  font-family: inherit;
  font-size: rem(24px);
  font-weight: 300;
  color: #fff;
  background: transparent;

  @include media-query(540) {
    height: 61px;
    font-size: rem(32px);
  }

  &::placeholder {
    color: #999;
    opacity: 1; // firefox native placeholder style has opacity < 1
  }

  &:focus::placeholder {
    color: #777;
    opacity: 1;
  }

  // webkit input doesn't inherit font-smoothing from ancestors
  -webkit-font-smoothing: antialiased;

  // remove `x`
  &::-ms-clear {
    display: none;
  }
}


================================================
FILE: src/views/components/task-item/index.js
================================================
export { default } from './task-item';


================================================
FILE: src/views/components/task-item/task-item.js
================================================
import React, { Component } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Button from '../button';
import Icon from '../icon';

import './task-item.css';


export class TaskItem extends Component {
  constructor() {
    super(...arguments);

    this.state = {editing: false};

    this.edit = this.edit.bind(this);
    this.handleKeyUp = this.handleKeyUp.bind(this);
    this.remove = this.remove.bind(this);
    this.save = this.save.bind(this);
    this.stopEditing = this.stopEditing.bind(this);
    this.toggleStatus = this.toggleStatus.bind(this);
  }

  edit() {
    this.setState({editing: true});
  }

  handleKeyUp(event) {
    if (event.keyCode === 13) {
      this.save(event);
    }
    else if (event.keyCode === 27) {
      this.stopEditing();
    }
  }

  remove() {
    this.props.removeTask(this.props.task);
  }

  save(event) {
    if (this.state.editing) {
      const { task } = this.props;
      const title = event.target.value.trim();

      if (title.length && title !== task.title) {
        this.props.updateTask(task, {title});
      }

      this.stopEditing();
    }
  }

  stopEditing() {
    this.setState({editing: false});
  }

  toggleStatus() {
    const { task } = this.props;
    this.props.updateTask(task, {completed: !task.completed});
  }

  renderTitle(task) {
    return (
      <div className="task-item__title" tabIndex="0">
        {task.title}
      </div>
    );
  }

  renderTitleInput(task) {
    return (
      <input
        autoComplete="off"
        autoFocus
        className="task-item__input"
        defaultValue={task.title}
        maxLength="64"
        onKeyUp={this.handleKeyUp}
        type="text"
      />
    );
  }

  render() {
    const { editing } = this.state;
    const { task } = this.props;

    let containerClasses = classNames('task-item', {
      'task-item--completed': task.completed,
      'task-item--editing': editing
    });

    return (
      <div className={containerClasses} tabIndex="0">
        <div className="cell">
          <Button
            className={classNames('btn--icon', 'task-item__button', {'active': task.completed, 'hide': editing})}
            onClick={this.toggleStatus}>
            <Icon name="done" />
          </Button>
        </div>

        <div className="cell">
          {editing ? this.renderTitleInput(task) : this.renderTitle(task)}
        </div>

        <div className="cell">
          <Button
            className={classNames('btn--icon', 'task-item__button', {'hide': editing})}
            onClick={this.edit}>
            <Icon name="mode_edit" />
          </Button>
          <Button
            className={classNames('btn--icon', 'task-item__button', {'hide': !editing})}
            onClick={this.stopEditing}>
            <Icon name="clear" />
          </Button>
          <Button
            className={classNames('btn--icon', 'task-item__button', {'hide': editing})}
            onClick={this.remove}>
            <Icon name="delete" />
          </Button>
        </div>
      </div>
    );
  }
}

TaskItem.propTypes = {
  removeTask: PropTypes.func.isRequired,
  task: PropTypes.object.isRequired,
  updateTask: PropTypes.func.isRequired
};


export default TaskItem;


================================================
FILE: src/views/components/task-item/task-item.scss
================================================
@import 'views/styles/shared';


.task-item {
  display: flex;
  outline: none;
  border-bottom: 1px dotted #666;
  height: 60px;
  overflow: hidden;
  color: #fff;
  font-size: rem(18px);
  font-weight: 300;

  @include media-query(540) {
    font-size: rem(24px);
  }
}

.task-item--editing {
  border-bottom: 1px dotted #ccc;
}


//=====================================
//  Cells
//-------------------------------------
.cell {
  &:first-child,
  &:last-child {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
  }

  &:first-child {
    padding-right: 20px;
  }

  &:nth-child(2) {
    flex: 1;
    padding-right: 30px;
    overflow: hidden;
  }
}


//=====================================
//  Buttons
//-------------------------------------
.task-item__button {
  margin-left: 5px;
  background: #2a2a2a;

  &:first-child {
    margin: 0;
  }

  color: #555;

  &:hover {
    color: #999;
  }

  &:active {
    background: #262626;
  }

  &.active {
    color: #85bf6b;
  }
}


//=====================================
//  Title (static)
//-------------------------------------
.task-item__title {
  display: inline-block;
  position: relative;
  max-width: 100%;
  line-height: 60px;
  outline: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;

  &:after {
    position: absolute;
    left: 0;
    bottom: 0;
    border-top: 2px solid #85bf6b;
    width: 0;
    height: 46%;
    content: '';
  }

  .task-item--completed & {
    color: #666;
  }

  .task-item--completed &:after {
    width: 100%;
  }
}


//=====================================
//  Title (input)
//-------------------------------------
.task-item__input {
  outline: none;
  border: 0;
  padding: 0;
  width: 100%;
  height: 60px;
  color: inherit;
  font: inherit;
  background: transparent;

  // hide `x`
  &::-ms-clear {
    display: none;
  }
}


================================================
FILE: src/views/components/task-list/index.js
================================================
export { default } from './task-list';


================================================
FILE: src/views/components/task-list/task-list.js
================================================
import React from 'react';
import { List } from 'immutable';
import PropTypes from 'prop-types';
import TaskItem from '../task-item';

import './task-list.css';


const TaskList = ({removeTask, tasks, updateTask}) => {
  let taskItems = tasks.map((task, index) => {
    return (
      <TaskItem
        removeTask={removeTask}
        key={index}
        task={task}
        updateTask={updateTask}
      />
    );
  });

  return (
    <div className="task-list">
      {taskItems}
    </div>
  );
};

TaskList.propTypes = {
  removeTask: PropTypes.func.isRequired,
  tasks: PropTypes.instanceOf(List),
  updateTask: PropTypes.func.isRequired
};


export default TaskList;


================================================
FILE: src/views/components/task-list/task-list.scss
================================================
.task-list {
  border-top: 1px dotted #666;
}


================================================
FILE: src/views/pages/sign-in/index.js
================================================
export { default } from './sign-in-page';


================================================
FILE: src/views/pages/sign-in/sign-in-page.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { authActions } from 'src/auth';
import Button from 'src/views/components/button';

import './sign-in-page.css';


const SignInPage = ({signInWithGithub, signInWithGoogle, signInWithTwitter}) => {
  return (
    <div className="g-row sign-in">
      <div className="g-col">
        <h1 className="sign-in__heading">Sign in</h1>
        <Button className="sign-in__button" onClick={signInWithGithub}>GitHub</Button>
        <Button className="sign-in__button" onClick={signInWithGoogle}>Google</Button>
        <Button className="sign-in__button" onClick={signInWithTwitter}>Twitter</Button>
      </div>
    </div>
  );
};

SignInPage.propTypes = {
  signInWithGithub: PropTypes.func.isRequired,
  signInWithGoogle: PropTypes.func.isRequired,
  signInWithTwitter: PropTypes.func.isRequired
};


//=====================================
//  CONNECT
//-------------------------------------

const mapDispatchToProps = {
  signInWithGithub: authActions.signInWithGithub,
  signInWithGoogle: authActions.signInWithGoogle,
  signInWithTwitter: authActions.signInWithTwitter
};

export default withRouter(
    connect(
    null,
    mapDispatchToProps
  )(SignInPage)
);


================================================
FILE: src/views/pages/sign-in/sign-in-page.scss
================================================
@import 'views/styles/shared';


.sign-in {
  margin-top: 90px;
  max-width: 300px;
}

.sign-in__heading {
  margin-bottom: 36px;
  font-size: 30px;
  font-weight: 300;
  text-align: center;
}

.sign-in__button {
  margin-bottom: 10px;
  border: 1px solid #555;
  width: 100%;
  height: 48px;
  font-family: inherit;
  font-size: rem(18px);
  line-height: 48px;
  color: #999;

  &:hover {
    border: 2px solid #aaa;
    line-height: 46px;
  }
}


================================================
FILE: src/views/pages/tasks/index.js
================================================
export { default } from './tasks-page';


================================================
FILE: src/views/pages/tasks/tasks-page.js
================================================
import React from 'react';
import { List } from 'immutable';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { taskActions, getVisibleTasks } from 'src/tasks';
import TaskFilters from 'src/views/components/task-filters';
import TaskForm from 'src/views/components/task-form';
import TaskList from 'src/views/components/task-list';


const TasksPage = ({createTask, location, removeTask, tasks, updateTask}) => {
  const params = new URLSearchParams(location.search);
  const filter = params.get('filter');

  return (
    <div className="g-row">
      <div className="g-col">
        <TaskForm handleSubmit={createTask} />
      </div>

      <div className="g-col">
        <TaskFilters filter={filter} />
        <TaskList
          filter={filter}
          removeTask={removeTask}
          tasks={tasks}
          updateTask={updateTask}
        />
      </div>
    </div>
  );
};

TasksPage.propTypes = {
  createTask: PropTypes.func.isRequired,
  filterTasks: PropTypes.func.isRequired,
  location: PropTypes.object.isRequired,
  removeTask: PropTypes.func.isRequired,
  tasks: PropTypes.instanceOf(List),
  updateTask: PropTypes.func.isRequired
};


//=====================================
//  CONNECT
//-------------------------------------

const mapStateToProps = state => ({
  tasks: getVisibleTasks(state)
});

const mapDispatchToProps = {
  createTask: taskActions.createTask,
  filterTasks: taskActions.filterTasks,
  removeTask: taskActions.removeTask,
  updateTask: taskActions.updateTask
};

export default withRouter(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(TasksPage)
);


================================================
FILE: src/views/styles/_grid.scss
================================================
.g-row {
  @include grid-row;
}

.g-col {
  @include grid-column;
  width: 100%;
}


================================================
FILE: src/views/styles/_settings.scss
================================================
$base-background-color: #222 !default;
$base-font-color: #999 !default;
$base-font-family: 'aktiv-grotesk-std', Helvetica Neue, Arial, sans-serif !default;
$base-font-size: 18px !default;
$base-line-height: 24px !default;


//=====================================
//  Grid
//-------------------------------------
$grid-max-width: 810px !default;


================================================
FILE: src/views/styles/_shared.scss
================================================
@import
'./settings',
'minx/src/settings',
'minx/src/functions',
'minx/src/mixins';


================================================
FILE: src/views/styles/styles.scss
================================================
@import
'./shared',
'minx/src/reset',
'minx/src/elements',
'./grid';


html {
  overflow-y: scroll;
}

body {
  padding-bottom: 120px;
}

a {
  color: inherit;
  text-decoration: none;
}

::selection {
  background: rgba(200,200,255,.1);
}

.hide {
  display: none !important;
}
Download .txt
gitextract_if41cgni/

├── .firebaserc
├── .gitignore
├── LICENSE
├── README.md
├── circle.yml
├── firebase.json
├── firebase.rules.json
├── package.json
├── public/
│   ├── index.html
│   └── manifest.json
└── src/
    ├── auth/
    │   ├── actions.js
    │   ├── auth.js
    │   ├── index.js
    │   ├── reducer.js
    │   ├── sagas.js
    │   └── selectors.js
    ├── firebase/
    │   ├── config.js
    │   ├── firebase-list.js
    │   ├── firebase.js
    │   └── index.js
    ├── history.js
    ├── index.js
    ├── reducers.js
    ├── register-service-worker.js
    ├── sagas.js
    ├── store.js
    ├── tasks/
    │   ├── actions.js
    │   ├── index.js
    │   ├── reducer.js
    │   ├── sagas.js
    │   ├── selectors.js
    │   ├── task-list.js
    │   └── task.js
    └── views/
        ├── app/
        │   ├── app.js
        │   └── index.js
        ├── components/
        │   ├── button/
        │   │   ├── button.js
        │   │   ├── button.scss
        │   │   ├── button.spec.js
        │   │   └── index.js
        │   ├── github-logo/
        │   │   ├── github-logo.js
        │   │   └── index.js
        │   ├── header/
        │   │   ├── header.js
        │   │   ├── header.scss
        │   │   └── index.js
        │   ├── icon/
        │   │   ├── icon.js
        │   │   ├── icon.spec.js
        │   │   └── index.js
        │   ├── require-auth-route/
        │   │   ├── index.js
        │   │   └── require-auth-route.js
        │   ├── require-unauth-route/
        │   │   ├── index.js
        │   │   └── require-unauth-route.js
        │   ├── task-filters/
        │   │   ├── index.js
        │   │   ├── task-filters.js
        │   │   └── task-filters.scss
        │   ├── task-form/
        │   │   ├── index.js
        │   │   ├── task-form.js
        │   │   └── task-form.scss
        │   ├── task-item/
        │   │   ├── index.js
        │   │   ├── task-item.js
        │   │   └── task-item.scss
        │   └── task-list/
        │       ├── index.js
        │       ├── task-list.js
        │       └── task-list.scss
        ├── pages/
        │   ├── sign-in/
        │   │   ├── index.js
        │   │   ├── sign-in-page.js
        │   │   └── sign-in-page.scss
        │   └── tasks/
        │       ├── index.js
        │       └── tasks-page.js
        └── styles/
            ├── _grid.scss
            ├── _settings.scss
            ├── _shared.scss
            └── styles.scss
Download .txt
SYMBOL INDEX (42 symbols across 13 files)

FILE: src/auth/auth.js
  function initAuth (line 5) | function initAuth(dispatch) {

FILE: src/auth/reducer.js
  function authReducer (line 12) | function authReducer(state = new AuthState(), {payload, type}) {

FILE: src/auth/selectors.js
  function isAuthenticated (line 4) | function isAuthenticated(state) {

FILE: src/firebase/firebase-list.js
  class FirebaseList (line 4) | class FirebaseList {
    method constructor (line 5) | constructor(actions, modelClass) {
    method path (line 10) | get path() {
    method path (line 14) | set path(value) {
    method push (line 18) | push(value) {
    method remove (line 25) | remove(key) {
    method update (line 32) | update(key, value) {
    method subscribe (line 39) | subscribe(emit) {
    method unwrapSnapshot (line 69) | unwrapSnapshot(snapshot) {

FILE: src/index.js
  function render (line 19) | function render(Component) {

FILE: src/register-service-worker.js
  function register (line 21) | function register() {
  function registerValidSW (line 46) | function registerValidSW(swUrl) {
  function checkValidServiceWorker (line 75) | function checkValidServiceWorker(swUrl) {
  function unregister (line 102) | function unregister() {

FILE: src/store.js
  function configureStore (line 9) | function configureStore() {

FILE: src/tasks/reducer.js
  function tasksReducer (line 11) | function tasksReducer(state = new TasksState(), {payload, type}) {

FILE: src/tasks/sagas.js
  function subscribe (line 9) | function subscribe() {

FILE: src/tasks/selectors.js
  function getTasks (line 4) | function getTasks(state) {
  function getTaskFilter (line 8) | function getTaskFilter(state) {
  function getTaskList (line 12) | function getTaskList(state) {

FILE: src/views/components/github-logo/github-logo.js
  function GitHubLogo (line 4) | function GitHubLogo() {

FILE: src/views/components/task-form/task-form.js
  class TaskForm (line 7) | class TaskForm extends Component {
    method constructor (line 12) | constructor() {
    method clearInput (line 22) | clearInput() {
    method handleChange (line 26) | handleChange(event) {
    method handleKeyUp (line 30) | handleKeyUp(event) {
    method handleSubmit (line 34) | handleSubmit(event) {
    method render (line 41) | render() {

FILE: src/views/components/task-item/task-item.js
  class TaskItem (line 10) | class TaskItem extends Component {
    method constructor (line 11) | constructor() {
    method edit (line 24) | edit() {
    method handleKeyUp (line 28) | handleKeyUp(event) {
    method remove (line 37) | remove() {
    method save (line 41) | save(event) {
    method stopEditing (line 54) | stopEditing() {
    method toggleStatus (line 58) | toggleStatus() {
    method renderTitle (line 63) | renderTitle(task) {
    method renderTitleInput (line 71) | renderTitleInput(task) {
    method render (line 85) | render() {
Condensed preview — 72 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (55K chars).
[
  {
    "path": ".firebaserc",
    "chars": 57,
    "preview": "{\n  \"projects\": {\n    \"default\": \"todo-redux-saga\"\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 521,
    "preview": "#======================================\n# Directories\n#--------------------------------------\nbuild/\ndist/\ncoverage/\nnod"
  },
  {
    "path": "LICENSE",
    "chars": 1090,
    "preview": "MIT License\n\nCopyright (c) 2016 Richard Park (objectiv@gmail.com)\n\nPermission is hereby granted, free of charge, to any "
  },
  {
    "path": "README.md",
    "chars": 2028,
    "preview": "[![CircleCI](https://circleci.com/gh/r-park/todo-redux-saga.svg?style=shield&circle-token=dc7e150ab97aab05db8f8da4b58744"
  },
  {
    "path": "circle.yml",
    "chars": 264,
    "preview": "machine:\n  node:\n    version: 8.1\n\ndependencies:\n  pre:\n    - rm -rf node_modules\n\ntest:\n  override:\n    - npm run build"
  },
  {
    "path": "firebase.json",
    "chars": 672,
    "preview": "{\n  \"database\": {\n    \"rules\": \"firebase.rules.json\"\n  },\n\n  \"hosting\": {\n    \"public\": \"build\",\n    \"headers\": [\n      "
  },
  {
    "path": "firebase.rules.json",
    "chars": 176,
    "preview": "{\n  \"rules\": {\n    \"tasks\": {\n      \"$uid\": {\n        \".read\": \"auth !== null && auth.uid === $uid\",\n        \".write\": \""
  },
  {
    "path": "package.json",
    "chars": 1805,
    "preview": "{\n  \"name\": \"todo-redux-saga\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Todo app with React, Redux, Redux-Saga, and Fireb"
  },
  {
    "path": "public/index.html",
    "chars": 655,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-wid"
  },
  {
    "path": "public/manifest.json",
    "chars": 333,
    "preview": "{\n  \"short_name\": \"Todo Redux Saga\",\n  \"name\": \"Todo app with React, Redux, Redux-Saga, and Firebase\",\n  \"icons\": [\n    "
  },
  {
    "path": "src/auth/actions.js",
    "chars": 1091,
    "preview": "import firebase from 'firebase/app';\n\n\nexport const authActions = {\n  SIGN_IN: 'SIGN_IN',\n  SIGN_IN_FAILED: 'SIGN_IN_FAI"
  },
  {
    "path": "src/auth/auth.js",
    "chars": 436,
    "preview": "import { firebaseAuth } from 'src/firebase';\nimport { authActions } from './actions';\n\n\nexport function initAuth(dispatc"
  },
  {
    "path": "src/auth/index.js",
    "chars": 210,
    "preview": "export { authActions } from './actions';\nexport { initAuth } from './auth';\nexport { authReducer } from './reducer';\nexp"
  },
  {
    "path": "src/auth/reducer.js",
    "chars": 604,
    "preview": "import { Record } from 'immutable';\nimport { authActions } from './actions';\n\n\nexport const AuthState = new Record({\n  a"
  },
  {
    "path": "src/auth/sagas.js",
    "chars": 1259,
    "preview": "import { call, fork, put, take } from 'redux-saga/effects';\nimport { firebaseAuth } from 'src/firebase';\nimport history "
  },
  {
    "path": "src/auth/selectors.js",
    "chars": 316,
    "preview": "import { createSelector } from 'reselect';\n\n\nexport function isAuthenticated(state) {\n  return state.auth.authenticated;"
  },
  {
    "path": "src/firebase/config.js",
    "chars": 241,
    "preview": "export const firebaseConfig = {\n  apiKey: 'AIzaSyCUll5AyYba1XL8NDYKZ51RGt90KofQo6c',\n  authDomain: 'todo-redux-saga.fire"
  },
  {
    "path": "src/firebase/firebase-list.js",
    "chars": 1656,
    "preview": "import { firebaseDb } from './firebase';\n\n\nexport class FirebaseList {\n  constructor(actions, modelClass) {\n    this._ac"
  },
  {
    "path": "src/firebase/firebase.js",
    "chars": 295,
    "preview": "import firebase from 'firebase/app';\n\nimport 'firebase/auth';\nimport 'firebase/database';\n\nimport { firebaseConfig } fro"
  },
  {
    "path": "src/firebase/index.js",
    "chars": 116,
    "preview": "export { firebaseApp, firebaseAuth, firebaseDb } from './firebase';\nexport { FirebaseList } from './firebase-list';\n"
  },
  {
    "path": "src/history.js",
    "chars": 92,
    "preview": "import createHistory from 'history/createBrowserHistory';\n\n\nexport default createHistory();\n"
  },
  {
    "path": "src/index.js",
    "chars": 954,
    "preview": "import './views/styles/styles.css';\n\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport { Provider } fr"
  },
  {
    "path": "src/reducers.js",
    "chars": 279,
    "preview": "import { combineReducers } from 'redux';\nimport { routerReducer } from 'react-router-redux';\nimport { authReducer } from"
  },
  {
    "path": "src/register-service-worker.js",
    "chars": 4022,
    "preview": "// In production, we register a service worker to serve assets from local cache.\n\n// This lets the app load faster on su"
  },
  {
    "path": "src/sagas.js",
    "chars": 208,
    "preview": "import { all } from 'redux-saga/effects'\nimport { authSagas } from './auth';\nimport { taskSagas } from './tasks';\n\n\nexpo"
  },
  {
    "path": "src/store.js",
    "chars": 897,
    "preview": "import { routerMiddleware } from 'react-router-redux';\nimport { applyMiddleware, compose, createStore } from 'redux';\nim"
  },
  {
    "path": "src/tasks/actions.js",
    "chars": 1627,
    "preview": "export const taskActions = {\n  CREATE_TASK: 'CREATE_TASK',\n  CREATE_TASK_FAILED: 'CREATE_TASK_FAILED',\n  CREATE_TASK_FUL"
  },
  {
    "path": "src/tasks/index.js",
    "chars": 167,
    "preview": "export { taskActions } from './actions';\nexport { tasksReducer } from './reducer';\nexport { taskSagas } from './sagas';\n"
  },
  {
    "path": "src/tasks/reducer.js",
    "chars": 953,
    "preview": "import { List, Record } from 'immutable';\nimport { taskActions } from './actions';\n\n\nexport const TasksState = new Recor"
  },
  {
    "path": "src/tasks/sagas.js",
    "chars": 2397,
    "preview": "import { LOCATION_CHANGE } from 'react-router-redux';\nimport { eventChannel } from 'redux-saga';\nimport { call, cancel, "
  },
  {
    "path": "src/tasks/selectors.js",
    "chars": 695,
    "preview": "import { createSelector } from 'reselect';\n\n\nexport function getTasks(state) {\n  return state.tasks;\n}\n\nexport function "
  },
  {
    "path": "src/tasks/task-list.js",
    "chars": 345,
    "preview": "import { FirebaseList } from 'src/firebase';\nimport { taskActions } from './actions';\nimport { Task } from './task';\n\n\ne"
  },
  {
    "path": "src/tasks/task.js",
    "chars": 122,
    "preview": "import { Record } from 'immutable';\n\n\nexport const Task = new Record({\n  completed: false,\n  key: null,\n  title: null\n})"
  },
  {
    "path": "src/views/app/app.js",
    "chars": 1208,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { withRoute"
  },
  {
    "path": "src/views/app/index.js",
    "chars": 33,
    "preview": "export { default } from './app';\n"
  },
  {
    "path": "src/views/components/button/button.js",
    "chars": 554,
    "preview": "import React from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\n\nimport './button.cs"
  },
  {
    "path": "src/views/components/button/button.scss",
    "chars": 273,
    "preview": "@import 'views/styles/shared';\n\n\n.btn {\n  @include button-base;\n  outline: none;\n  border: 0;\n  padding: 0;\n  overflow: "
  },
  {
    "path": "src/views/components/button/button.spec.js",
    "chars": 1701,
    "preview": "import React from 'react';\nimport { render, shallow } from 'enzyme';\nimport Button from './button';\n\n\ndescribe('Button',"
  },
  {
    "path": "src/views/components/button/index.js",
    "chars": 36,
    "preview": "export { default } from './button';\n"
  },
  {
    "path": "src/views/components/github-logo/github-logo.js",
    "chars": 574,
    "preview": "import React from 'react';\n\n\nexport default function GitHubLogo() {\n  return (\n    <svg viewBox=\"0 0 20 20\">\n      <path"
  },
  {
    "path": "src/views/components/github-logo/index.js",
    "chars": 41,
    "preview": "export { default } from './github-logo';\n"
  },
  {
    "path": "src/views/components/header/header.js",
    "chars": 842,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport Button from '../button';\nimport GitHubLogo from '."
  },
  {
    "path": "src/views/components/header/header.scss",
    "chars": 924,
    "preview": "@import 'views/styles/shared';\n\n\n.header {\n  padding: 10px 0;\n  height: 60px;\n  overflow: hidden;\n  line-height: 40px;\n}"
  },
  {
    "path": "src/views/components/header/index.js",
    "chars": 36,
    "preview": "export { default } from './header';\n"
  },
  {
    "path": "src/views/components/icon/icon.js",
    "chars": 371,
    "preview": "import React from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\n\n\nconst Icon = ({cla"
  },
  {
    "path": "src/views/components/icon/icon.spec.js",
    "chars": 542,
    "preview": "import React from 'react';\nimport { render, shallow } from 'enzyme';\nimport Icon from './icon';\n\n\ndescribe('Icon', () =>"
  },
  {
    "path": "src/views/components/icon/index.js",
    "chars": 34,
    "preview": "export { default } from './icon';\n"
  },
  {
    "path": "src/views/components/require-auth-route/index.js",
    "chars": 48,
    "preview": "export { default } from './require-auth-route';\n"
  },
  {
    "path": "src/views/components/require-auth-route/require-auth-route.js",
    "chars": 447,
    "preview": "import React from 'react';\nimport { Route, Redirect } from 'react-router-dom'\n\n\nconst RequireAuthRoute = ({component: Co"
  },
  {
    "path": "src/views/components/require-unauth-route/index.js",
    "chars": 50,
    "preview": "export { default } from './require-unauth-route';\n"
  },
  {
    "path": "src/views/components/require-unauth-route/require-unauth-route.js",
    "chars": 444,
    "preview": "import React from 'react';\nimport { Route, Redirect } from 'react-router-dom'\n\n\nconst RequireUnauthRoute = ({component: "
  },
  {
    "path": "src/views/components/task-filters/index.js",
    "chars": 42,
    "preview": "export { default } from './task-filters';\n"
  },
  {
    "path": "src/views/components/task-filters/task-filters.js",
    "chars": 633,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { NavLink } from 'react-router-dom';\n\nimport './ta"
  },
  {
    "path": "src/views/components/task-filters/task-filters.scss",
    "chars": 540,
    "preview": "@import 'views/styles/shared';\n\n\n.task-filters {\n  @include clearfix;\n  margin-bottom: 45px;\n  padding-left: 1px;\n  font"
  },
  {
    "path": "src/views/components/task-form/index.js",
    "chars": 39,
    "preview": "export { default } from './task-form';\n"
  },
  {
    "path": "src/views/components/task-form/task-form.js",
    "chars": 1321,
    "preview": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\n\nimport './task-form.css';\n\n\nexport class "
  },
  {
    "path": "src/views/components/task-form/task-form.scss",
    "chars": 813,
    "preview": "@import 'views/styles/shared';\n\n\n.task-form {\n  margin: 40px 0 10px;\n\n  @include media-query(540) {\n    margin: 80px 0 2"
  },
  {
    "path": "src/views/components/task-item/index.js",
    "chars": 39,
    "preview": "export { default } from './task-item';\n"
  },
  {
    "path": "src/views/components/task-item/task-item.js",
    "chars": 3261,
    "preview": "import React, { Component } from 'react';\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimpor"
  },
  {
    "path": "src/views/components/task-item/task-item.scss",
    "chars": 1865,
    "preview": "@import 'views/styles/shared';\n\n\n.task-item {\n  display: flex;\n  outline: none;\n  border-bottom: 1px dotted #666;\n  heig"
  },
  {
    "path": "src/views/components/task-list/index.js",
    "chars": 39,
    "preview": "export { default } from './task-list';\n"
  },
  {
    "path": "src/views/components/task-list/task-list.js",
    "chars": 674,
    "preview": "import React from 'react';\nimport { List } from 'immutable';\nimport PropTypes from 'prop-types';\nimport TaskItem from '."
  },
  {
    "path": "src/views/components/task-list/task-list.scss",
    "chars": 46,
    "preview": ".task-list {\n  border-top: 1px dotted #666;\n}\n"
  },
  {
    "path": "src/views/pages/sign-in/index.js",
    "chars": 42,
    "preview": "export { default } from './sign-in-page';\n"
  },
  {
    "path": "src/views/pages/sign-in/sign-in-page.js",
    "chars": 1320,
    "preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\nimport { withRoute"
  },
  {
    "path": "src/views/pages/sign-in/sign-in-page.scss",
    "chars": 447,
    "preview": "@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-bott"
  },
  {
    "path": "src/views/pages/tasks/index.js",
    "chars": 40,
    "preview": "export { default } from './tasks-page';\n"
  },
  {
    "path": "src/views/pages/tasks/tasks-page.js",
    "chars": 1691,
    "preview": "import React from 'react';\nimport { List } from 'immutable';\nimport PropTypes from 'prop-types';\nimport { connect } from"
  },
  {
    "path": "src/views/styles/_grid.scss",
    "chars": 83,
    "preview": ".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",
    "chars": 346,
    "preview": "$base-background-color: #222 !default;\n$base-font-color: #999 !default;\n$base-font-family: 'aktiv-grotesk-std', Helvetic"
  },
  {
    "path": "src/views/styles/_shared.scss",
    "chars": 84,
    "preview": "@import\n'./settings',\n'minx/src/settings',\n'minx/src/functions',\n'minx/src/mixins';\n"
  },
  {
    "path": "src/views/styles/styles.scss",
    "chars": 279,
    "preview": "@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-"
  }
]

About this extraction

This page contains the full source code of the r-park/todo-redux-saga GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 72 files (47.3 KB), approximately 14.2k tokens, and a symbol index with 42 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!