├── config
├── flow
│ ├── css.js.flow
│ └── file.js.flow
├── env.js
├── polyfills.js
├── babel.prod.js
├── babel.dev.js
├── paths.js
├── eslint.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── favicon.ico
├── src
├── components
│ ├── forms
│ │ ├── index.js
│ │ ├── Button.css
│ │ ├── Input.js
│ │ ├── TextArea.css
│ │ ├── TextArea.js
│ │ ├── Input.css
│ │ └── Button.js
│ ├── DummyCard.js
│ ├── ListHeader.css
│ ├── ListHeader.js
│ ├── DummyCard.css
│ ├── List.css
│ ├── Footer.css
│ ├── Header.js
│ ├── Footer.js
│ ├── Header.css
│ ├── KanbanBoard.css
│ ├── icons
│ │ ├── Arrow.js
│ │ ├── Github.js
│ │ └── Logo.js
│ ├── AddNewList.css
│ ├── Card.css
│ ├── AddNewCard.css
│ ├── List.js
│ ├── AddNewCard.js
│ ├── AddNewList.js
│ ├── KanbanBoard.js
│ └── Card.js
├── index.js
├── libs
│ └── localStorage.js
├── App.js
├── App.css
└── stores
│ └── kanban.js
├── index.js
├── .gitignore
├── .travis.yml
├── scripts
├── utils
│ ├── WatchMissingNodeModulesPlugin.js
│ ├── chrome.applescript
│ └── prompt.js
├── build.js
└── start.js
├── .github
└── workflows
│ └── static.yml
├── README.md
├── index.html
└── package.json
/config/flow/css.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 |
--------------------------------------------------------------------------------
/config/flow/file.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 | declare export default string;
3 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sarmadsangi/offline-kanban/HEAD/favicon.ico
--------------------------------------------------------------------------------
/src/components/forms/index.js:
--------------------------------------------------------------------------------
1 | export Input from './Input';
2 | export TextArea from './TextArea';
3 | export Button from './Button';
4 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var server = require('pushstate-server');
2 |
3 | server.start({
4 | port: (process.env.PORT || 5000),
5 | directory: './build',
6 | file: '/index.html'
7 | });
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # production
7 | build
8 |
9 | # misc
10 | .DS_Store
11 | npm-debug.log
12 |
--------------------------------------------------------------------------------
/src/components/forms/Button.css:
--------------------------------------------------------------------------------
1 | .button {
2 | background: #72f1ab;
3 | padding: 10px 15px;
4 | border-radius: 4px;
5 | font-size: 14px;
6 | color: #fff;
7 | font-weight: 600;
8 | margin: 5px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/forms/Input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Input.css';
3 |
4 | const Input = (props) => (
5 |
6 | );
7 |
8 | export default Input;
9 |
--------------------------------------------------------------------------------
/src/components/forms/TextArea.css:
--------------------------------------------------------------------------------
1 | .text_area {
2 | width: 100%;
3 | font-size: 15px;
4 | border: 1px solid #f5f5f5;
5 | padding: 10px;
6 | box-sizing: border-box;
7 | border-radius: 4px;
8 | outline: none;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/forms/TextArea.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './TextArea.css';
3 |
4 | const TextArea = (props) => (
5 |
6 | );
7 |
8 | export default TextArea;
9 |
--------------------------------------------------------------------------------
/src/components/DummyCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './DummyCard.css';
3 |
4 | const DummyCard = (props) => (
5 |
8 | );
9 |
10 | export default DummyCard;
11 |
--------------------------------------------------------------------------------
/src/components/forms/Input.css:
--------------------------------------------------------------------------------
1 | .input {
2 | width: 100%;
3 | height: 100%;
4 | font-size: 25px;
5 | font-weight: 200;
6 | border: 1px solid #f5f5f5;
7 | box-sizing: content-box;
8 | border-radius: 4px;
9 | padding: 3px 10px;
10 | outline: none;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ListHeader.css:
--------------------------------------------------------------------------------
1 | .list_header {
2 | width: 100%;
3 | height: 50px;
4 | display: flex;
5 | align-items: center;
6 | margin-bottom: 20px;
7 | box-sizing: border-box;
8 | justify-content: space-between;
9 | font-size: 25px;
10 | font-weight: 200;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/forms/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Button.css';
3 |
4 | const Button = (props) => {
5 | const { children, ...rest } = props;
6 | return {children}
7 | };
8 |
9 | export default Button;
10 |
--------------------------------------------------------------------------------
/src/components/ListHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './ListHeader.css';
3 |
4 | const ListHeader = (props) => (
5 |
9 | );
10 |
11 | export default ListHeader;
12 |
--------------------------------------------------------------------------------
/src/components/DummyCard.css:
--------------------------------------------------------------------------------
1 | .card {
2 | min-height: 50px;
3 | padding-right: 6px;
4 | padding-left: 6px;
5 | margin-bottom: 1px;
6 | background-color: #f9f9f9;
7 | padding: 10px;
8 | margin-bottom: 10px;
9 | border: 1px dashed #eee;
10 | position: relative;;;
11 | border-radius: 4px;
12 | pointer-events: none;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/List.css:
--------------------------------------------------------------------------------
1 | .list {
2 | position: relative;
3 | min-width: 200px;
4 | width: 300px;
5 | max-width: 80vh;
6 | box-sizing: border-box;
7 | flex: 1;
8 | margin-right: 5px;
9 | overflow-y: scroll;
10 | border-radius: 4px;
11 | padding: 15px;
12 | border-right: 2px dashed #f5f5f5;
13 | }
14 |
15 | .list_cards_section {
16 | min-height: 100px;
17 | position: relative;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | padding: 0px 15px;
3 | border-top: 1px solid #eee;
4 | height: 30px;
5 | font-size: 12px;
6 | font-weight: 200;
7 | display: flex;
8 | align-items: center;
9 | justify-content: space-between;
10 | }
11 |
12 | .footer_github {
13 | width: 20px;
14 | height: 20px;
15 | fill: #999;
16 | }
17 |
18 | .footer_link {
19 | font-weight: 400;
20 | color: #999;
21 | padding: 5px;
22 | font-size: 13px;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Header.css';
3 | import Logo from 'components/icons/Logo';
4 |
5 | const Header = (props) => (
6 |
7 |
8 |
9 |
Offline Kanban
10 |
11 |
12 | Total Tasks #{props.totalTasks}
13 |
14 |
15 | );
16 |
17 | export default Header;
18 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Footer.css';
3 | import GithubIcon from 'components/icons/Github';
4 |
5 | const Footer = () => (
6 |
12 | );
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/config/env.js:
--------------------------------------------------------------------------------
1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
2 | // injected into the application via DefinePlugin in Webpack configuration.
3 |
4 | var REACT_APP = /^REACT_APP_/i;
5 | var NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development');
6 |
7 | module.exports = Object
8 | .keys(process.env)
9 | .filter(key => REACT_APP.test(key))
10 | .reduce((env, key) => {
11 | env['process.env.' + key] = JSON.stringify(process.env[key]);
12 | return env;
13 | }, {
14 | 'process.env.NODE_ENV': NODE_ENV
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | height: 65px;
3 | box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.05);
4 | padding: 15px;
5 | box-sizing: border-box;
6 | border-bottom: 1px solid #f9f9f9;
7 | display: flex;
8 | align-items: center;
9 | justify-content: space-between;
10 | }
11 |
12 | .logo_container {
13 | display: flex;
14 | height: 100%;
15 | align-items: center;
16 | }
17 |
18 | .logo {
19 | width: 35px;
20 | fill: #72f1ab;
21 | }
22 |
23 | .logo_container > h2 {
24 | margin: 0;
25 | padding: 0;
26 | font-size: 25px;
27 | font-weight: 200;
28 | margin-left: 15px;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/KanbanBoard.css:
--------------------------------------------------------------------------------
1 | .board {
2 | position: relative;
3 | height: calc(100vh - 95px);
4 | box-sizing: border-box;
5 | overflow-y: hidden;
6 | }
7 |
8 | .lists_wrapper {
9 | display: flex;
10 | flex-flow: row;
11 | height: 100%;
12 | position: absolute;
13 | left: 0;
14 | padding: 15px;
15 | }
16 |
17 | .slider_wrapper {
18 | display: flex;
19 | flex-flow: row;
20 | height: 100%;
21 | left: 0;
22 | overflow: hidden;
23 | margin-top: 15px;
24 | }
25 |
26 | :global(.slick-track) {
27 | display: flex !important;
28 | }
29 |
30 | :global(.slick-arrow) {
31 | display: none !important;
32 | }
33 |
--------------------------------------------------------------------------------
/config/polyfills.js:
--------------------------------------------------------------------------------
1 | if (typeof Promise === 'undefined') {
2 | // Rejection tracking prevents a common issue where React gets into an
3 | // inconsistent state due to an error, but it gets swallowed by a Promise,
4 | // and the user has no idea what causes React's erratic future behavior.
5 | require('promise/lib/rejection-tracking').enable();
6 | window.Promise = require('promise/lib/es6-extensions.js');
7 | }
8 |
9 | // fetch() polyfill for making API calls.
10 | require('whatwg-fetch');
11 |
12 | // Object.assign() is commonly used with React.
13 | // It will use the native implementation if it's present and isn't buggy.
14 | Object.assign = require('object-assign');
15 |
--------------------------------------------------------------------------------
/src/components/icons/Arrow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Arrow = (props) => (
4 |
11 | );
12 |
13 | Arrow.defaultProps = {
14 | fill: '#72f1ab',
15 | width: 20,
16 | height: 20,
17 | };
18 |
19 | export default Arrow;
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const runtime = require('offline-plugin/runtime');
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import MobileDetect from 'mobile-detect';
6 | import App from './App';
7 |
8 | // TODO: Remove this global when seperate components for mobile are created
9 | window.__md = new MobileDetect(window.navigator.userAgent);
10 |
11 | ReactDOM.render(
12 | ,
13 | document.getElementById('root')
14 | );
15 |
16 | runtime.install({
17 | onUpdateReady: () => {
18 | // Tells to new SW to take control immediately
19 | runtime.applyUpdate();
20 | },
21 | onUpdated: () => {
22 | // Reload the webpage to load into the new version
23 | window.location.reload();
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/AddNewList.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | width: 300px;
4 | margin-right: 5px;
5 | border-radius: 4px;
6 | padding: 15px;
7 | }
8 |
9 | .container {
10 | width: 100%;
11 | height: 50px;
12 | display: flex;
13 | align-items: center;
14 | padding: 10px;
15 | margin-bottom: 10px;
16 | box-sizing: border-box;
17 | justify-content: space-between;
18 | cursor: pointer;
19 | font-size: 25px;
20 | font-weight: 200;
21 | color: #ccc;
22 | border-radius: 4px;
23 | }
24 |
25 | .container:hover {
26 | background: #f9f9f9;
27 | }
28 |
29 | .add_button {
30 | margin-bottom: 10px;
31 | box-sizing: border-box;
32 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
33 | background-color: #fff;
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Card.css:
--------------------------------------------------------------------------------
1 | .card {
2 | min-height: 50px;
3 | padding-right: 6px;
4 | padding-left: 6px;
5 | margin-bottom: 1px;
6 | background-color: #fff;
7 | align-items: center;
8 | padding: 10px;
9 | margin-bottom: 10px;
10 | border: 1px solid #eee;
11 | transition: all .2s cubic-bezier(0.165, 0.84, 0.44, 1);
12 | position: relative;
13 | border-radius: 4px;
14 | cursor: pointer;
15 | font-size: 17px;
16 | font-weight: 200;
17 | }
18 |
19 | .card:hover {
20 | border: 1px solid #85d2ff;
21 | }
22 |
23 | .controls {
24 | display: flex;
25 | align-items: center;
26 | justify-content: flex-end;
27 | }
28 |
29 | .goBack, .goNext {
30 | padding: 5px 10px;
31 | }
32 |
33 | .goBack > svg {
34 | transform: rotate(180deg);
35 | }
36 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - v5.11
4 | before_script:
5 | - npm run build
6 | deploy:
7 | provider: heroku
8 | api_key:
9 | secure: lQW4HAK1CO/GMrwgmbyfm+FJoM51CFr3whb7CDqRxN8z30RLvxTZYajA12U4OEUG3lAsWSei1f1YscgBvyB59zeE7fhLIR0cAzbDBwK5ip8fokJLCC2atfH/NlnWt/Y6BehWKeAWpQvS1CewZ4fxn9EqL5hFeGzCOIdhn2wudwGp1wVaPpAHPfCxSjAvpVFigWfyUeD8IJVx/lsRYVkcSkQCtJ1gezNYLTpVEJnseX4La1LMscOssPomtSHPyCIt+mK4ZndZGfnfGeE/ibUePomBG+qSIQAaPf769NWzALlI4lClwjZBdc4NkNXjT4Ue0XKSdYbmEdCUIu8al1Y4C/YQNVltCCY+beF+mTAGbcHgl/pupErqw+vkAnCc8X5IYrwSVa2u+WkK3v/naut0pNVlTri7Gdei2GnQXUou86JUnF3l0YtrPyjlhHKL0N0WWJv9poMvbJhEvLdcxqQcqFYFM5XdCH8F4lLFHE3NHEdj979kAM0gteT0XxSMGZJaKJ//KArtqnMNmiC/4fD0xSST0W+GvNdyNhlEHyrcJUrozOGsiUzeLLpInWA/iWQePSdjEVW34DifbTyeRuAyf8ofgFARg8zTModz6iq7Eac/worOo2yVCmkyfQm45QOcM58kDxdXtTDQVwEgX2T/NbHCsyA+P29zgBWusZCMZCA=
10 | app: offline-kanban
11 | skip_cleanup: true
12 | on:
13 | repo: sarmadsangi/offline-kanban
14 |
--------------------------------------------------------------------------------
/src/components/AddNewCard.css:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 100%;
3 | display: block;
4 | padding: 10px;
5 | margin-bottom: 10px;
6 | box-sizing: border-box;
7 | cursor: pointer;
8 | border-radius: 4px;
9 | font-weight: 200;
10 | font-size: 15px;
11 | color: #ccc;
12 | border: 1px dashed #f5f5f5;
13 | }
14 |
15 | .container:hover {
16 | background: #f9f9f9;
17 | border: 1px dashed #ccc;
18 | color: #aaa;
19 | }
20 |
21 | .form {
22 | width: 100%;
23 | display: flex;
24 | align-items: center;
25 | padding: 10px;
26 | border-radius: 4px;
27 | margin-bottom: 10px;
28 | box-sizing: border-box;
29 | justify-content: space-between;
30 | border:1px solid #f5f5f5;
31 | cursor: pointer;
32 | flex-direction: column;
33 | }
34 |
35 | .form_footer {
36 | display: flex;
37 | width: 100%;
38 | }
39 |
40 | .add_button {
41 | margin-bottom: 10px;
42 | box-sizing: border-box;
43 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
44 | background-color: #fff;
45 | }
46 |
--------------------------------------------------------------------------------
/scripts/utils/WatchMissingNodeModulesPlugin.js:
--------------------------------------------------------------------------------
1 | // This Webpack plugin ensures `npm install ` forces a project rebuild.
2 | // We’re not sure why this isn't Webpack's default behavior.
3 | // See https://github.com/facebookincubator/create-react-app/issues/186.
4 |
5 | function WatchMissingNodeModulesPlugin(nodeModulesPath) {
6 | this.nodeModulesPath = nodeModulesPath;
7 | }
8 |
9 | WatchMissingNodeModulesPlugin.prototype.apply = function (compiler) {
10 | compiler.plugin('emit', (compilation, callback) => {
11 | var missingDeps = compilation.missingDependencies;
12 | var nodeModulesPath = this.nodeModulesPath;
13 |
14 | // If any missing files are expected to appear in node_modules...
15 | if (missingDeps.some(file => file.indexOf(nodeModulesPath) !== -1)) {
16 | // ...tell webpack to watch node_modules recursively until they appear.
17 | compilation.contextDependencies.push(nodeModulesPath);
18 | }
19 |
20 | callback();
21 | });
22 | }
23 |
24 | module.exports = WatchMissingNodeModulesPlugin;
25 |
--------------------------------------------------------------------------------
/scripts/utils/chrome.applescript:
--------------------------------------------------------------------------------
1 | on run argv
2 | set theURL to item 1 of argv
3 |
4 | tell application "Chrome"
5 |
6 | if (count every window) = 0 then
7 | make new window
8 | end if
9 |
10 | -- Find a tab currently running the debugger
11 | set found to false
12 | set theTabIndex to -1
13 | repeat with theWindow in every window
14 | set theTabIndex to 0
15 | repeat with theTab in every tab of theWindow
16 | set theTabIndex to theTabIndex + 1
17 | if theTab's URL is theURL then
18 | set found to true
19 | exit repeat
20 | end if
21 | end repeat
22 |
23 | if found then
24 | exit repeat
25 | end if
26 | end repeat
27 |
28 | if found then
29 | tell theTab to reload
30 | set index of theWindow to 1
31 | set theWindow's active tab index to theTabIndex
32 | else
33 | tell window 1
34 | activate
35 | make new tab with properties {URL:theURL}
36 | end tell
37 | end if
38 | end tell
39 | end run
40 |
--------------------------------------------------------------------------------
/scripts/utils/prompt.js:
--------------------------------------------------------------------------------
1 | var rl = require('readline');
2 |
3 | // Convention: "no" should be the conservative choice.
4 | // If you mistype the answer, we'll always take it as a "no".
5 | // You can control the behavior on with `isYesDefault`.
6 | module.exports = function (question, isYesDefault) {
7 | if (typeof isYesDefault !== 'boolean') {
8 | throw new Error('Provide explicit boolean isYesDefault as second argument.');
9 | }
10 | return new Promise(resolve => {
11 | var rlInterface = rl.createInterface({
12 | input: process.stdin,
13 | output: process.stdout,
14 | });
15 |
16 | var hint = isYesDefault === true ? '[Y/n]' : '[y/N]';
17 | var message = question + ' ' + hint + '\n';
18 |
19 | rlInterface.question(message, function(answer) {
20 | rlInterface.close();
21 |
22 | var useDefault = answer.trim().length === 0;
23 | if (useDefault) {
24 | return resolve(isYesDefault);
25 | }
26 |
27 | var isYes = answer.match(/^(yes|y)$/i);
28 | return resolve(isYes);
29 | });
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/src/libs/localStorage.js:
--------------------------------------------------------------------------------
1 | import PouchDB from 'pouchdb';
2 |
3 | // set revs to 2 to save space (since out data is not super critical here).
4 | const db = new PouchDB('offline-kanban', {revs_limit: 2});
5 |
6 | // add new doc or update if it exists
7 | export function createOrUpdate(newDoc) {
8 | db
9 | .get(newDoc._id)
10 | .then((doc) => {
11 | return create({
12 | ...newDoc,
13 | _rev: doc._rev,
14 | updated_at: Date.now(),
15 | });
16 | })
17 | .then(response => console.log(response))
18 | .catch(err => {
19 | console.log(err);
20 | return create(newDoc);
21 | });
22 | }
23 |
24 | export function create(doc) {
25 | return db.put(doc);
26 | }
27 |
28 | export function get(doc) {
29 | db
30 | .get(doc._id)
31 | .then(doc => console.log(doc))
32 | .catch(err => console.log(err));
33 | }
34 |
35 | // getAll
36 | // TODO: descending is not really working out well, in future will use .query
37 | // to do proper position sorting of lists.
38 | export function getAll(cb) {
39 | db
40 | .allDocs({
41 | include_docs: true,
42 | descending: true,
43 | })
44 | .then(response => cb(response.rows.map(row => row.doc)))
45 | .catch(err => console.log(err));
46 | }
47 |
--------------------------------------------------------------------------------
/config/babel.prod.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // Don't try to find .babelrc because we want to force this configuration.
3 | babelrc: false,
4 | presets: [
5 | // Latest stable ECMAScript features
6 | require.resolve('babel-preset-latest'),
7 | // JSX, Flow
8 | require.resolve('babel-preset-react')
9 | ],
10 | plugins: [
11 | require.resolve('babel-plugin-transform-decorators-legacy'),
12 | require.resolve('babel-plugin-transform-export-extensions'),
13 |
14 | // class { handleClick = () => { } }
15 | require.resolve('babel-plugin-transform-class-properties'),
16 | // { ...todo, completed: true }
17 | require.resolve('babel-plugin-transform-object-rest-spread'),
18 | // function* () { yield 42; yield 43; }
19 | [require.resolve('babel-plugin-transform-regenerator'), {
20 | // Async functions are converted to generators by babel-preset-latest
21 | async: false
22 | }],
23 | // Polyfills the runtime needed for async/await and generators
24 | [require.resolve('babel-plugin-transform-runtime'), {
25 | helpers: false,
26 | polyfill: false,
27 | regenerator: true
28 | }],
29 | // Optimization: hoist JSX that never changes out of render()
30 | require.resolve('babel-plugin-transform-react-constant-elements')
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/config/babel.dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // Don't try to find .babelrc because we want to force this configuration.
3 | babelrc: false,
4 | // This is a feature of `babel-loader` for webpack (not Babel itself).
5 | // It enables caching results in OS temporary directory for faster rebuilds.
6 | cacheDirectory: true,
7 | presets: [
8 | // Latest stable ECMAScript features
9 | require.resolve('babel-preset-latest'),
10 | // JSX, Flow
11 | require.resolve('babel-preset-react')
12 | ],
13 | plugins: [
14 | require.resolve('babel-plugin-transform-decorators-legacy'),
15 | require.resolve('babel-plugin-transform-export-extensions'),
16 | // class { handleClick = () => { } }
17 | require.resolve('babel-plugin-transform-class-properties'),
18 | // { ...todo, completed: true }
19 | require.resolve('babel-plugin-transform-object-rest-spread'),
20 | // function* () { yield 42; yield 43; }
21 | [require.resolve('babel-plugin-transform-regenerator'), {
22 | // Async functions are converted to generators by babel-preset-latest
23 | async: false
24 | }],
25 | // Polyfills the runtime needed for async/await and generators
26 | [require.resolve('babel-plugin-transform-runtime'), {
27 | helpers: false,
28 | polyfill: false,
29 | regenerator: true
30 | }]
31 | ]
32 | };
33 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["master"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | - name: Install Dependencies
37 | run: npm install
38 | - name: Build
39 | run: npm run build
40 | - name: Upload artifact
41 | uses: actions/upload-pages-artifact@v3
42 | with:
43 | # Upload entire repository
44 | path: './build'
45 | - name: Deploy to GitHub Pages
46 | id: deployment
47 | uses: actions/deploy-pages@v4
48 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Header from 'components/Header.js';
3 | import KanbanBoard from 'components/KanbanBoard.js';
4 | import Footer from 'components/Footer.js';
5 | import styles from './App.css';
6 | import { observer } from 'mobx-react';
7 | import KanbanStore from 'stores/kanban';
8 |
9 | @observer
10 | class App extends Component {
11 | renderLoading() {
12 | return (
13 |
14 |
15 |
18 |
19 |
20 | );
21 | }
22 |
23 | render() {
24 |
25 | if (KanbanStore.loading) {
26 | return (
27 |
28 | {this.renderLoading()}
29 |
30 | );
31 | }
32 |
33 | return (
34 |
35 |
36 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: sans-serif;
3 | -webkit-text-size-adjust: 100%;
4 | -ms-text-size-adjust: 100%;
5 | height: 100%;
6 | width: 100%;
7 | }
8 |
9 | body {
10 | color: #666;
11 | -webkit-font-smoothing: antialiased;
12 | padding: 0;
13 | margin: 0;
14 | }
15 |
16 | a {
17 | text-decoration: none;
18 | }
19 |
20 | .loader_wrapper {
21 | position: absolute;
22 | width: 100%;
23 | height: 100%;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 |
29 | .loader {
30 | position: relative;
31 | margin: 0 auto;
32 | width: 100px;
33 | }
34 |
35 | .loader:before {
36 | content: '';
37 | display: block;
38 | padding-top: 100%;
39 | }
40 |
41 | .circular {
42 | animation: rotate 2s linear infinite;
43 | height: 100%;
44 | transform-origin: center center;
45 | width: 100%;
46 | position: absolute;
47 | top: 0;
48 | bottom: 0;
49 | left: 0;
50 | right: 0;
51 | margin: auto;
52 | }
53 |
54 | .path {
55 | stroke-dasharray: 1, 200;
56 | stroke-dashoffset: 0;
57 | animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
58 | stroke-linecap: round;
59 | }
60 |
61 | @keyframes rotate {
62 | 100% {
63 | transform: rotate(360deg);
64 | }
65 | }
66 |
67 | @keyframes dash {
68 | 0% {
69 | stroke-dasharray: 1, 200;
70 | stroke-dashoffset: 0;
71 | }
72 | 50% {
73 | stroke-dasharray: 89, 200;
74 | stroke-dashoffset: -35px;
75 | }
76 | 100% {
77 | stroke-dasharray: 89, 200;
78 | stroke-dashoffset: -124px;
79 | }
80 | }
81 |
82 | @keyframes color {
83 | 100%,
84 | 0% {
85 | stroke: #008744;
86 | }
87 | 40% {
88 | stroke: #0057e7;
89 | }
90 | 66% {
91 | stroke: #008744;
92 | }
93 | 80%,
94 | 90% {
95 | stroke: #ffa700;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Offline Kanban
2 |
3 | A browser-based Kanban board that works offline, powered by modern web technologies.
4 |
5 | [](https://travis-ci.org/sarmadsangi/offline-kanban)
6 |
7 | ## Getting Started
8 |
9 | ```bash
10 | npm install
11 | npm run dev
12 | ```
13 |
14 | ## Production Build
15 | ```bash
16 | npm run build
17 | ```
18 |
19 | ## Deployment
20 |
21 | ### GitHub Pages Deployment
22 |
23 | 1. Go to your repository settings
24 | 2. Navigate to "Pages" in the sidebar
25 | 3. Under "Source", select "GitHub Actions"
26 | 4. Push your code to the master/main branch
27 |
28 | The site will be automatically built and deployed to [https://sarmadsangi.github.io/offline-kanban](https://sarmadsangi.github.io/offline-kanban)
29 |
30 | Note: Make sure your repository is public and GitHub Pages is enabled in your repository settings.
31 |
32 | ## Architecture
33 |
34 | - **Frontend**: React.js with CSS Modules for component-scoped styling
35 | - **State Management**: MobX for predictable state updates
36 | - **Offline Storage**: PouchDB (IndexedDB/WebSQL) for persistent local storage
37 | - **Offline Capability**: Service Workers and AppCache for offline asset serving
38 | - **Continuous Integration**: Travis CI with automated Heroku deployment
39 |
40 | ### Key Features
41 |
42 | - Fully functional offline-first architecture
43 | - Real-time state persistence
44 | - Drag-and-drop card management
45 | - Automatic state synchronization
46 | - Component-isolated styling
47 |
48 | ## Roadmap
49 |
50 | 1. Mobile-responsive design optimization
51 | 2. Store architecture refactoring
52 | 3. Drag-and-drop operation improvements
53 | 4. Performance optimizations
54 | 5. Enhanced sorting algorithm implementation
55 |
56 | ## Development Status
57 |
58 | [](https://travis-ci.org/sarmadsangi/offline-kanban)
59 |
60 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | // TODO: we can split this file into several files (pre-eject, post-eject, test)
2 | // and use those instead. This way we don't need to branch here.
3 |
4 | var path = require('path');
5 |
6 | // True after ejecting, false when used as a dependency
7 | var isEjected = (
8 | path.resolve(path.join(__dirname, '..')) ===
9 | path.resolve(process.cwd())
10 | );
11 |
12 | // Are we developing create-react-app locally?
13 | var isInCreateReactAppSource = (
14 | process.argv.some(arg => arg.indexOf('--debug-template') > -1)
15 | );
16 |
17 | function resolveOwn(relativePath) {
18 | return path.resolve(__dirname, relativePath);
19 | }
20 |
21 | function resolveApp(relativePath) {
22 | return path.resolve(relativePath);
23 | }
24 |
25 | if (isInCreateReactAppSource) {
26 | // create-react-app development: we're in ./config/
27 | module.exports = {
28 | appBuild: resolveOwn('../build'),
29 | appHtml: resolveOwn('../template/index.html'),
30 | appFavicon: resolveOwn('../template/favicon.ico'),
31 | appPackageJson: resolveOwn('../package.json'),
32 | appSrc: resolveOwn('../template/src'),
33 | appNodeModules: resolveOwn('../node_modules'),
34 | ownNodeModules: resolveOwn('../node_modules')
35 | };
36 | } else if (!isEjected) {
37 | // before eject: we're in ./node_modules/react-scripts/config/
38 | module.exports = {
39 | appBuild: resolveApp('build'),
40 | appHtml: resolveApp('index.html'),
41 | appFavicon: resolveApp('favicon.ico'),
42 | appPackageJson: resolveApp('package.json'),
43 | appSrc: resolveApp('src'),
44 | appNodeModules: resolveApp('node_modules'),
45 | // this is empty with npm3 but node resolution searches higher anyway:
46 | ownNodeModules: resolveOwn('../node_modules')
47 | };
48 | } else {
49 | // after eject: we're in ./config/
50 | module.exports = {
51 | appBuild: resolveApp('build'),
52 | appHtml: resolveApp('index.html'),
53 | appFavicon: resolveApp('favicon.ico'),
54 | appPackageJson: resolveApp('package.json'),
55 | appSrc: resolveApp('src'),
56 | appNodeModules: resolveApp('node_modules'),
57 | ownNodeModules: resolveApp('node_modules')
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/List.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styles from './List.css';
3 | import Card from 'components/Card';
4 | import AddNewCard from 'components/AddNewCard';
5 | import ListHeader from 'components/ListHeader';
6 | import { observer } from 'mobx-react';
7 |
8 | @observer
9 | class List extends Component {
10 | constructor(props) {
11 | super(props);
12 | this.onCardDropIntoEmptyList = this.onCardDropIntoEmptyList.bind(this);
13 | this.onDragOver = this.onDragOver.bind(this);
14 | this.handleDropOnCard = this.handleDropOnCard.bind(this);
15 | }
16 |
17 | getCards() {
18 | const { list, moveCardToPreviousList, moveCardToNextList } = this.props;
19 | const { cards } = list;
20 | let cardElements;
21 | if (cards.length) {
22 | cardElements = cards.map(card => (
23 | {
29 | moveCardToPreviousList(card, list);
30 | }}
31 | moveCardToNextList={() => {
32 | moveCardToNextList(card, list);
33 | }}
34 | />
35 | ));
36 | }
37 |
38 | return cardElements;
39 | }
40 |
41 | // only takes care of empty list drop
42 | // everything else happens inside handleDropOnCard
43 | onCardDropIntoEmptyList(e) {
44 | e.preventDefault();
45 | const {list_id, ...dropProps} = JSON.parse(e.dataTransfer.getData('text'));
46 | const { list, moveCardToAnotherList } = this.props;
47 |
48 | if (!list.cards || !list.cards.length) {
49 | moveCardToAnotherList(dropProps, list_id, list._id);
50 | }
51 | }
52 |
53 | onDragOver(e) {
54 | e.preventDefault();
55 | }
56 |
57 | handleDropOnCard(card, prevListId, nextListId) {
58 | this.props.moveCardToAnotherList(card, prevListId, nextListId);
59 | }
60 |
61 | render() {
62 | const { list, handleAddNewCardToList } = this.props;
63 |
64 | return (
65 |
66 |
67 |
71 | {this.getCards()}
72 |
73 |
74 |
75 | );
76 | }
77 | };
78 |
79 | export default List;
80 |
--------------------------------------------------------------------------------
/src/components/AddNewCard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { TextArea, Button } from 'components/forms';
3 | import styles from './AddNewCard.css';
4 |
5 | class AddNewCard extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.handleKeyDown = this.handleKeyDown.bind(this);
9 | this.hide = this.hide.bind(this);
10 | this.handleInputValueChange = this.handleInputValueChange.bind(this);
11 | this.handleAddNewCardClick = this.handleAddNewCardClick.bind(this);
12 | this.displayInputBox = this.displayInputBox.bind(this);
13 | this.state = {
14 | displayInputBox: false,
15 | cardTitle: '',
16 | };
17 | }
18 |
19 | handleKeyDown(e) {
20 | if (e.keyCode === 27) {
21 | this.hide();
22 | }
23 |
24 | if (e.keyCode === 13) {
25 | this.handleAddNewCardClick(e);
26 | }
27 | }
28 |
29 | hide() {
30 | this.setState({
31 | displayInputBox: false
32 | });
33 | }
34 |
35 | handleInputValueChange(event) {
36 | this.setState({
37 | cardTitle: event.target.value
38 | });
39 | }
40 |
41 | displayInputBox() {
42 | this.setState({
43 | displayInputBox: true
44 | });
45 | }
46 |
47 | handleAddNewCardClick(e) {
48 | e.preventDefault();
49 | const { cardTitle } = this.state;
50 | this.props.handleAddNewCard(cardTitle);
51 |
52 | this.setState({
53 | cardTitle: '',
54 | displayInputBox: false,
55 | })
56 | }
57 |
58 | renderInitial() {
59 | return (
60 |
61 | Add New Task
62 |
63 | );
64 | }
65 |
66 | renderInputBox() {
67 | const { cardTitle } = this.state;
68 |
69 | return (
70 |
83 | );
84 | }
85 |
86 | render() {
87 | const { displayInputBox } = this.state;
88 |
89 | if (displayInputBox) {
90 | return this.renderInputBox();
91 | }
92 |
93 | return this.renderInitial();
94 | }
95 | };
96 |
97 | export default AddNewCard;
98 |
--------------------------------------------------------------------------------
/src/components/icons/Github.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Github = (props) => (
4 |
28 | );
29 |
30 | export default Github;
31 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Offline Kanban | Simple Kanban board for your daily chores
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/AddNewList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Input, Button } from 'components/forms';
3 | import styles from './AddNewList.css';
4 |
5 | class AddNewList extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.hideOnESC = this.hideOnESC.bind(this);
9 | this.hide = this.hide.bind(this);
10 | this.handleInputValueChange = this.handleInputValueChange.bind(this);
11 | this.handleAddNewListClick = this.handleAddNewListClick.bind(this);
12 | this.displayInputBox = this.displayInputBox.bind(this);
13 | this.state = {
14 | displayInputBox: false,
15 | listName: '',
16 | };
17 | }
18 |
19 | hideOnESC(e) {
20 | if (e.keyCode === 27) {
21 | this.hide();
22 | }
23 | }
24 |
25 | hide() {
26 | this.setState({
27 | displayInputBox: false
28 | });
29 | }
30 |
31 | handleInputValueChange(event) {
32 | this.setState({
33 | listName: event.target.value
34 | });
35 | }
36 |
37 | displayInputBox() {
38 | this.setState({
39 | displayInputBox: true
40 | });
41 | }
42 |
43 | handleAddNewListClick(e) {
44 | e.preventDefault();
45 | const { listName } = this.state;
46 | this.props.handleAddNewList(listName);
47 |
48 | this.setState({
49 | listName: '',
50 | displayInputBox: false,
51 | })
52 | }
53 |
54 | renderInitial() {
55 | return (
56 |
57 | Add new list
58 |
59 | );
60 | }
61 |
62 | renderInputBox() {
63 | const { listName } = this.state;
64 |
65 | return (
66 |
77 | );
78 | }
79 |
80 | render() {
81 | const { displayInputBox } = this.state;
82 |
83 | if (displayInputBox) {
84 | return (
85 |
86 | {this.renderInputBox()}
87 |
88 | );
89 | }
90 |
91 | return (
92 |
93 | {this.renderInitial()}
94 |
95 | );
96 | }
97 | };
98 |
99 | export default AddNewList;
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "offline-kanban",
3 | "version": "0.0.1",
4 | "private": true,
5 | "main": "index.js",
6 | "homepage": "https://sarmadsangi.github.io/offline-kanban",
7 | "devDependencies": {
8 | "babel-core": "6.14.0",
9 | "babel-eslint": "6.1.2",
10 | "babel-loader": "6.2.5",
11 | "babel-plugin-transform-class-properties": "6.11.5",
12 | "babel-plugin-transform-decorators": "^6.13.0",
13 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
14 | "babel-plugin-transform-export-extensions": "^6.8.0",
15 | "babel-plugin-transform-object-rest-spread": "6.8.0",
16 | "babel-plugin-transform-react-constant-elements": "6.9.1",
17 | "babel-plugin-transform-regenerator": "6.14.0",
18 | "babel-plugin-transform-runtime": "6.12.0",
19 | "babel-preset-latest": "6.14.0",
20 | "babel-preset-react": "6.11.1",
21 | "babel-runtime": "6.11.6",
22 | "case-sensitive-paths-webpack-plugin": "1.1.3",
23 | "chalk": "1.1.3",
24 | "connect-history-api-fallback": "1.3.0",
25 | "cross-spawn": "4.0.0",
26 | "css-loader": "0.23.1",
27 | "detect-port": "1.0.0",
28 | "eslint": "3.1.1",
29 | "eslint-loader": "1.4.1",
30 | "eslint-plugin-flowtype": "2.4.0",
31 | "eslint-plugin-import": "1.12.0",
32 | "eslint-plugin-jsx-a11y": "2.0.1",
33 | "eslint-plugin-react": "5.2.2",
34 | "extract-text-webpack-plugin": "1.0.1",
35 | "file-loader": "0.9.0",
36 | "filesize": "3.3.0",
37 | "fs-extra": "0.30.0",
38 | "gzip-size": "3.0.0",
39 | "html-webpack-plugin": "2.22.0",
40 | "http-proxy-middleware": "0.17.0",
41 | "json-loader": "0.5.4",
42 | "object-assign": "4.1.0",
43 | "opn": "4.0.2",
44 | "postcss-cssnext": "^2.5.2",
45 | "postcss-import": "^8.1.2",
46 | "postcss-loader": "0.9.1",
47 | "promise": "7.1.1",
48 | "recursive-readdir": "2.0.0",
49 | "rimraf": "2.5.4",
50 | "strip-ansi": "3.0.1",
51 | "style-loader": "0.13.1",
52 | "url-loader": "0.5.7",
53 | "webpack": "1.13.1",
54 | "webpack-dev-server": "1.14.1",
55 | "whatwg-fetch": "1.0.0"
56 | },
57 | "dependencies": {
58 | "lodash": "^4.15.0",
59 | "mobile-detect": "^1.3.3",
60 | "mobx": "^2.0.0",
61 | "mobx-react": "^3.0.0",
62 | "offline-plugin": "^3.4.2",
63 | "pouchdb": "^5.4.5",
64 | "pushstate-server": "^1.12.0",
65 | "react": "^15.3.1",
66 | "react-dom": "^15.3.1",
67 | "react-slick": "^0.14.5"
68 | },
69 | "scripts": {
70 | "dev": "node ./scripts/start.js",
71 | "build": "node ./scripts/build.js",
72 | "start": "node index.js"
73 | },
74 | "eslintConfig": {
75 | "extends": "./config/eslint.js"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/icons/Logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Logo = (props) => (
4 |
35 | );
36 |
37 | export default Logo;
38 |
--------------------------------------------------------------------------------
/src/components/KanbanBoard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styles from './KanbanBoard.css';
3 | import List from 'components/List';
4 | import AddNewList from 'components/AddNewList';
5 | import { observer } from 'mobx-react';
6 | import Slider from 'react-slick';
7 |
8 | var slideTimer = null;
9 |
10 | @observer
11 | class KanbanBoard extends Component {
12 | constructor(props) {
13 | super(props);
14 | this.handleCreateNewList = this.handleCreateNewList.bind(this);
15 | this.handleMoveCardToAnotherList = this.handleMoveCardToAnotherList.bind(this);
16 | this.transitionCardToNextList = this.transitionCardToNextList.bind(this);
17 | this.transitionCardToPreviousList = this.transitionCardToPreviousList.bind(this);
18 | }
19 |
20 | handleCreateNewList(listName) {
21 | this.props.createNewList(listName);
22 | }
23 |
24 | handleMoveCardToAnotherList(cardToMove, prevListId, nextListId) {
25 | this.props.moveCardToAnotherList(cardToMove, prevListId, nextListId);
26 | }
27 |
28 | transitionCardToNextList(card, list) {
29 | const { slideCount, currentSlide } = this.refs.slider.innerSlider.state;
30 |
31 | // check if there is next list, -2 to exclude ADD NEW LIST component
32 | // which part of slider but is not counted as list.
33 | if (currentSlide < slideCount-2) {
34 | this.refs.slider.slickNext();
35 | clearTimeout(slideTimer);
36 | slideTimer = setTimeout(() => this.props.moveCardToNextList(card, list), 300);
37 | }
38 | }
39 |
40 | transitionCardToPreviousList(card, list) {
41 | this.refs.slider.slickPrev();
42 | clearTimeout(slideTimer);
43 | slideTimer = setTimeout(() => this.props.moveCardToPreviousList(card, list), 300);
44 | }
45 |
46 | renderLists() {
47 | const {
48 | lists,
49 | createNewCard,
50 | } = this.props;
51 |
52 | const settings = {
53 | dots: false,
54 | infinite: false,
55 | speed: 300,
56 | slidesToShow: 1,
57 | nextArrow: '',
58 | swipe: true,
59 | prevArrow: '',
60 | slidesToScroll: 1,
61 | variableWidth: true,
62 | className: styles.slider_wrapper,
63 | };
64 |
65 | const listElements = lists.map(list => (
66 |
67 | createNewCard(cardName, list._id)}
70 | moveCardToAnotherList={this.handleMoveCardToAnotherList}
71 | moveCardToPreviousList={this.transitionCardToPreviousList}
72 | moveCardToNextList={this.transitionCardToNextList}
73 | />
74 |
75 | ));
76 |
77 | if (window.__md.isPhoneSized()) {
78 | return (
79 |
80 | {listElements}
81 |
84 |
85 | );
86 | }
87 |
88 | return (
89 |
90 | {listElements}
91 |
92 |
93 | );
94 | }
95 |
96 | render() {
97 | return (
98 |
99 | {this.renderLists()}
100 |
101 | )
102 | }
103 | };
104 |
105 | export default KanbanBoard;
106 |
--------------------------------------------------------------------------------
/src/components/Card.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import styles from './Card.css';
3 | import DummyCard from 'components/DummyCard';
4 | import ArrowIcon from 'components/icons/Arrow';
5 | import { observer } from 'mobx-react';
6 |
7 | export const constants = {
8 | CARD_ID: 'CARD_ID',
9 | };
10 |
11 | @observer
12 | class Card extends Component {
13 | constructor(props) {
14 | super(props);
15 | this.handleDrop = this.handleDrop.bind(this);
16 | this.hideDummy = this.hideDummy.bind(this);
17 | this.displayDummy = this.displayDummy.bind(this);
18 | this.handleCardDragStart = this.handleCardDragStart.bind(this);
19 | this.handleCardDragEnd = this.handleCardDragEnd.bind(this);
20 |
21 | this.state = {
22 | displayDummy: false,
23 | dragging: false,
24 | };
25 | }
26 |
27 | handleDrop(e) {
28 | e.preventDefault();
29 |
30 | const { handleDropOnCard, list_id, _id, pos } = this.props;
31 | const dropProps = JSON.parse(e.dataTransfer.getData('text'));
32 |
33 | // prepare new card props, some will be discarded in Store
34 | // TODO: improve this schema / move this to store as well.
35 | const newCard = {
36 | _id: dropProps._id,
37 | droppedAtCardId: _id,
38 | newPos: pos,
39 | title: dropProps.title,
40 | pos: dropProps.pos,
41 | created_at: dropProps.created_at,
42 | }
43 |
44 | handleDropOnCard(newCard, dropProps.list_id, list_id);
45 | this.hideDummy(e);
46 | }
47 |
48 | hideDummy(e) {
49 | // onDragLeave causes issues when there are nested elements inside drop zone
50 | // with this it will only trigger any change on dropzone element itself (parent)
51 | if (e.target.id === constants.CARD_ID) {
52 | this.setState({
53 | displayDummy: false,
54 | dragging: false,
55 | });
56 | }
57 | }
58 |
59 | // Don't display dummy when this card itself is being dragged
60 | displayDummy(e) {
61 | const { dragging } = this.state;
62 | if (!dragging) {
63 | this.setState({
64 | displayDummy: true,
65 | });
66 | }
67 | }
68 |
69 | handleCardDragStart(e) {
70 | e.dataTransfer.setData('text', JSON.stringify(this.props));
71 | this.setState({
72 | dragging: true,
73 | })
74 | }
75 |
76 | handleCardDragEnd(e) {
77 | this.setState({
78 | dragging: false,
79 | })
80 | }
81 |
82 | renderMobileControls() {
83 | return (
84 |
88 | );
89 | }
90 |
91 | render() {
92 | const { title, isMobileCard } = this.props;
93 | const { displayDummy } = this.state;
94 |
95 | return (
96 |
101 | {displayDummy ?
: ''}
102 |
107 |
{title}
108 | {isMobileCard ? this.renderMobileControls() : null}
109 |
110 |
111 | )
112 | }
113 | };
114 |
115 | export default Card;
116 |
--------------------------------------------------------------------------------
/src/stores/kanban.js:
--------------------------------------------------------------------------------
1 | import { observable, autorunAsync, computed, toJS } from 'mobx';
2 | import { createOrUpdate, getAll } from 'libs/localStorage';
3 | import _ from 'lodash';
4 |
5 | // Store that handles all the Kanban board logic
6 | const kanban = new class Kanban {
7 | @observable lists = [];
8 | @observable loading = false;
9 |
10 | constructor() {
11 | this.getLists = this.getLists.bind(this);
12 | this.createNewList = this.createNewList.bind(this);
13 | this.createNewCard = this.createNewCard.bind(this);
14 | this.addCardToList = this.addCardToList.bind(this);
15 | this.removeCardFromList = this.removeCardFromList.bind(this);
16 | this.moveCardToAnotherList = this.moveCardToAnotherList.bind(this);
17 | this.moveCardToNextList = this.moveCardToNextList.bind(this);
18 | this.moveCardToPreviousList = this.moveCardToPreviousList.bind(this);
19 | this.loading = true;
20 |
21 | // Get all board lists from pouchdb
22 | // 500ms delay to show smooth loading
23 | getAll((docs) => {
24 | setTimeout(() => {
25 | this.loading = false
26 | }, 500);
27 | this.lists = docs;
28 | });
29 | }
30 |
31 | getLists() {
32 | return _.sortBy(this.lists.toJS(), ['pos']);
33 | }
34 |
35 | // generate total number of tasks
36 | @computed get totalTasks() {
37 | const totalTasks = _.reduce(this.lists, (result, list) => {
38 | result += list.cards.length;
39 | return result;
40 | }, 0);
41 |
42 | return totalTasks;
43 | }
44 |
45 | // assign uniqueId, date etc to each list before add to board
46 | createNewList(listName) {
47 | const newPos = this.lists.length;
48 | this.lists.push({
49 | _id: _.uniqueId(listName),
50 | name: listName,
51 | pos: newPos,
52 | cards: [],
53 | created_at: Date.now(),
54 | });
55 | }
56 |
57 | // assign uniqueId, date, pos etc to each card before adding to list
58 | createNewCard(cardName, listId) {
59 | const cards = this.getListCards(listId);
60 | const newPos = cards.length !== 0 ? (cards[cards.length-1].pos + 1) : 0;
61 |
62 | this.addCardToList({
63 | _id: _.uniqueId(cardName),
64 | title: cardName,
65 | pos: newPos,
66 | created_at: Date.now(),
67 | }, listId);
68 | }
69 |
70 | // Add new or existing card to list
71 | addCardToList(cardToAdd, listId) {
72 | try {
73 | let cards = this.getListCards(listId);
74 |
75 | // pushing position of other cards down
76 | // TODO: proper sorted array implementation to be done later
77 | cards = cards.map(card => {
78 | if (cardToAdd.newPos <= card.pos) {
79 | card.pos += 1;
80 | }
81 | return card;
82 | });
83 |
84 | cards.push({
85 | _id: cardToAdd._id,
86 | title: cardToAdd.title,
87 | pos: cardToAdd.newPos || cardToAdd.pos,
88 | created_at: cardToAdd.created_at,
89 | });
90 |
91 | this.setListCards(listId, _.sortBy(cards, ['pos']));
92 | } catch (e) {
93 | this.setListCards(listId, [cardToAdd]);
94 | }
95 | }
96 |
97 | // remove card from list
98 | removeCardFromList(cardToRemove, listId) {
99 | let cards = _.reject(this.getListCards(listId), { _id: cardToRemove._id });
100 | this.setListCards(listId, cards);
101 | }
102 |
103 | // move card from one list to another
104 | // if card is being moved within same list then just repositioning it.
105 | moveCardToAnotherList(card, prevListId, nextListId) {
106 | if (prevListId === nextListId) {
107 | this.moveCardWithInSameList(card, prevListId);
108 | return;
109 | }
110 | this.addCardToList(card, nextListId);
111 | this.removeCardFromList(card, prevListId);
112 | }
113 |
114 | moveCardWithInSameList(cardToMove, listId) {
115 | let cards = this.getListCards(listId);
116 |
117 | // pushing position of other cards down and assigning new position to card that was moved
118 | // TODO: proper sorted array implementation to be done later
119 | cards = cards.map(card => {
120 | if (cardToMove.newPos <= card.pos) {
121 | card.pos += 1;
122 | }
123 |
124 | if (cardToMove._id === card._id) {
125 | card.pos = cardToMove.newPos;
126 | }
127 |
128 | return card;
129 | });
130 |
131 |
132 | this.setListCards(listId, _.sortBy(cards, ['pos']));
133 | }
134 |
135 | // get specified list cards
136 | getListCards(listId) {
137 | return _.find(this.lists, { _id: listId }).cards || [];
138 | }
139 |
140 | // set cards for specified list
141 | setListCards(listId, cards) {
142 | this.lists.filter(list => list._id === listId)[0].cards = cards;
143 | }
144 |
145 | moveCardToNextList(card, currentList) {
146 | const sortedLists = this.getLists();
147 | const nextList = _.find(sortedLists, { pos: currentList.pos + 1 });
148 | if (nextList) {
149 | this.moveCardToAnotherList(card, currentList._id, nextList._id);
150 | }
151 | }
152 |
153 | moveCardToPreviousList(card, currentList) {
154 | const sortedLists = this.getLists();
155 | const previousList = _.find(sortedLists, { pos: currentList.pos - 1 });
156 | if (previousList) {
157 | this.moveCardToAnotherList(card, currentList._id, previousList._id);
158 | }
159 | }
160 | }();
161 |
162 |
163 | // autorun with delay of 300ms when kanban list is updates
164 | // this is to save all the data to pouchdb and keep everything in sync
165 | // to provide better offline/lie-fi experience
166 | autorunAsync(() => {
167 | const lists = toJS(kanban.lists, false);
168 | lists.forEach(list => {
169 | createOrUpdate(list)
170 | });
171 | }, 300);
172 |
173 | export default kanban;
174 |
--------------------------------------------------------------------------------
/config/eslint.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/airbnb/javascript but less opinionated.
2 |
3 | // We use eslint-loader so even warnings are very visibile.
4 | // This is why we only use "WARNING" level for potential errors,
5 | // and we don't use "ERROR" level at all.
6 |
7 | // In the future, we might create a separate list of rules for production.
8 | // It would probably be more strict.
9 |
10 | module.exports = {
11 | root: true,
12 |
13 | parser: 'babel-eslint',
14 |
15 | // import plugin is termporarily disabled, scroll below to see why
16 | plugins: [/*'import', 'flowtype',*/ 'jsx-a11y', 'react'],
17 |
18 | env: {
19 | browser: true,
20 | commonjs: true,
21 | es6: true,
22 | node: true
23 | },
24 |
25 | parserOptions: {
26 | ecmaVersion: 6,
27 | sourceType: 'module',
28 | ecmaFeatures: {
29 | jsx: true,
30 | generators: true,
31 | experimentalObjectRestSpread: true
32 | }
33 | },
34 |
35 | settings: {
36 | 'import/ignore': [
37 | 'node_modules',
38 | '\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$',
39 | ],
40 | 'import/extensions': ['.js'],
41 | 'import/resolver': {
42 | node: {
43 | extensions: ['.js', '.json']
44 | }
45 | }
46 | },
47 |
48 | rules: {
49 | // http://eslint.org/docs/rules/
50 | 'array-callback-return': 'warn',
51 | 'default-case': ['warn', { commentPattern: '^no default$' }],
52 | 'dot-location': ['warn', 'property'],
53 | eqeqeq: ['warn', 'allow-null'],
54 | 'guard-for-in': 'warn',
55 | 'new-parens': 'warn',
56 | 'no-array-constructor': 'warn',
57 | 'no-caller': 'warn',
58 | 'no-cond-assign': ['warn', 'always'],
59 | 'no-const-assign': 'warn',
60 | 'no-control-regex': 'warn',
61 | 'no-delete-var': 'warn',
62 | 'no-dupe-args': 'warn',
63 | 'no-dupe-class-members': 'warn',
64 | 'no-dupe-keys': 'warn',
65 | 'no-duplicate-case': 'warn',
66 | 'no-empty-character-class': 'warn',
67 | 'no-empty-pattern': 'warn',
68 | 'no-eval': 'warn',
69 | 'no-ex-assign': 'warn',
70 | 'no-extend-native': 'warn',
71 | 'no-extra-bind': 'warn',
72 | 'no-extra-label': 'warn',
73 | 'no-fallthrough': 'warn',
74 | 'no-func-assign': 'warn',
75 | 'no-implied-eval': 'warn',
76 | 'no-invalid-regexp': 'warn',
77 | 'no-iterator': 'warn',
78 | 'no-label-var': 'warn',
79 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }],
80 | 'no-lone-blocks': 'warn',
81 | 'no-loop-func': 'warn',
82 | 'no-mixed-operators': ['warn', {
83 | groups: [
84 | ['&', '|', '^', '~', '<<', '>>', '>>>'],
85 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
86 | ['&&', '||'],
87 | ['in', 'instanceof']
88 | ],
89 | allowSamePrecedence: false
90 | }],
91 | 'no-multi-str': 'warn',
92 | 'no-native-reassign': 'warn',
93 | 'no-negated-in-lhs': 'warn',
94 | 'no-new-func': 'warn',
95 | 'no-new-object': 'warn',
96 | 'no-new-symbol': 'warn',
97 | 'no-new-wrappers': 'warn',
98 | 'no-obj-calls': 'warn',
99 | 'no-octal': 'warn',
100 | 'no-octal-escape': 'warn',
101 | 'no-redeclare': 'warn',
102 | 'no-regex-spaces': 'warn',
103 | 'no-restricted-syntax': [
104 | 'warn',
105 | 'LabeledStatement',
106 | 'WithStatement',
107 | ],
108 | 'no-return-assign': 'warn',
109 | 'no-script-url': 'warn',
110 | 'no-self-assign': 'warn',
111 | 'no-self-compare': 'warn',
112 | 'no-sequences': 'warn',
113 | 'no-shadow-restricted-names': 'warn',
114 | 'no-sparse-arrays': 'warn',
115 | 'no-this-before-super': 'warn',
116 | 'no-throw-literal': 'warn',
117 | 'no-undef': 'warn',
118 | 'no-unexpected-multiline': 'warn',
119 | 'no-unreachable': 'warn',
120 | 'no-unused-expressions': 'warn',
121 | 'no-unused-labels': 'warn',
122 | 'no-unused-vars': ['warn', { vars: 'local', args: 'none' }],
123 | 'no-use-before-define': ['warn', 'nofunc'],
124 | 'no-useless-computed-key': 'warn',
125 | 'no-useless-concat': 'warn',
126 | 'no-useless-constructor': 'warn',
127 | 'no-useless-escape': 'warn',
128 | 'no-useless-rename': ['warn', {
129 | ignoreDestructuring: false,
130 | ignoreImport: false,
131 | ignoreExport: false,
132 | }],
133 | 'no-with': 'warn',
134 | 'no-whitespace-before-property': 'warn',
135 | 'operator-assignment': ['warn', 'always'],
136 | radix: 'warn',
137 | 'require-yield': 'warn',
138 | 'rest-spread-spacing': ['warn', 'never'],
139 | strict: ['warn', 'never'],
140 | 'unicode-bom': ['warn', 'never'],
141 | 'use-isnan': 'warn',
142 | 'valid-typeof': 'warn',
143 |
144 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/
145 |
146 | // TODO: import rules are temporarily disabled because they don't play well
147 | // with how eslint-loader only checks the file you change. So if module A
148 | // imports module B, and B is missing a default export, the linter will
149 | // record this as an issue in module A. Now if you fix module B, the linter
150 | // will not be aware that it needs to re-lint A as well, so the error
151 | // will stay until the next restart, which is really confusing.
152 |
153 | // This is probably fixable with a patch to eslint-loader.
154 | // When file A is saved, we want to invalidate all files that import it
155 | // *and* that currently have lint errors. This should fix the problem.
156 |
157 | // 'import/default': 'warn',
158 | // 'import/export': 'warn',
159 | // 'import/named': 'warn',
160 | // 'import/namespace': 'warn',
161 | // 'import/no-amd': 'warn',
162 | // 'import/no-duplicates': 'warn',
163 | // 'import/no-extraneous-dependencies': 'warn',
164 | // 'import/no-named-as-default': 'warn',
165 | // 'import/no-named-as-default-member': 'warn',
166 | // 'import/no-unresolved': ['warn', { commonjs: true }],
167 |
168 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
169 | 'react/jsx-equals-spacing': ['warn', 'never'],
170 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }],
171 | 'react/jsx-no-undef': 'warn',
172 | 'react/jsx-pascal-case': ['warn', {
173 | allowAllCaps: true,
174 | ignore: [],
175 | }],
176 | 'react/jsx-uses-react': 'warn',
177 | 'react/jsx-uses-vars': 'warn',
178 | 'react/no-deprecated': 'warn',
179 | 'react/no-direct-mutation-state': 'warn',
180 | 'react/no-is-mounted': 'warn',
181 | 'react/react-in-jsx-scope': 'warn',
182 | 'react/require-render-return': 'warn',
183 |
184 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
185 | 'jsx-a11y/aria-role': 'warn',
186 | 'jsx-a11y/img-has-alt': 'warn',
187 | 'jsx-a11y/img-redundant-alt': 'warn',
188 | 'jsx-a11y/no-access-key': 'warn',
189 |
190 | // https://github.com/gajus/eslint-plugin-flowtype
191 | // 'flowtype/define-flow-type': 'warn',
192 | // 'flowtype/require-valid-file-annotation': 'warn',
193 | // 'flowtype/use-flow-type': 'warn'
194 | }
195 | };
196 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | // Do this as the first thing so that any code reading it knows the right env.
2 | process.env.NODE_ENV = 'production';
3 |
4 | var chalk = require('chalk');
5 | var fs = require('fs');
6 | var path = require('path');
7 | var filesize = require('filesize');
8 | var gzipSize = require('gzip-size').sync;
9 | var rimrafSync = require('rimraf').sync;
10 | var webpack = require('webpack');
11 | var config = require('../config/webpack.config.prod');
12 | var paths = require('../config/paths');
13 | var recursive = require('recursive-readdir');
14 | var stripAnsi = require('strip-ansi');
15 |
16 | // Input: /User/dan/app/build/static/js/main.82be8.js
17 | // Output: /static/js/main.js
18 | function removeFileNameHash(fileName) {
19 | return fileName
20 | .replace(paths.appBuild, '')
21 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3);
22 | }
23 |
24 | // Input: 1024, 2048
25 | // Output: "(+1 KB)"
26 | function getDifferenceLabel(currentSize, previousSize) {
27 | var FIFTY_KILOBYTES = 1024 * 50;
28 | var difference = currentSize - previousSize;
29 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0;
30 | if (difference >= FIFTY_KILOBYTES) {
31 | return chalk.red('+' + fileSize);
32 | } else if (difference < FIFTY_KILOBYTES && difference > 0) {
33 | return chalk.yellow('+' + fileSize);
34 | } else if (difference < 0) {
35 | return chalk.green(fileSize);
36 | } else {
37 | return '';
38 | }
39 | }
40 |
41 | // First, read the current file sizes in build directory.
42 | // This lets us display how much they changed later.
43 | recursive(paths.appBuild, (err, fileNames) => {
44 | var previousSizeMap = (fileNames || [])
45 | .filter(fileName => /\.(js|css)$/.test(fileName))
46 | .reduce((memo, fileName) => {
47 | var contents = fs.readFileSync(fileName);
48 | var key = removeFileNameHash(fileName);
49 | memo[key] = gzipSize(contents);
50 | return memo;
51 | }, {});
52 |
53 | // Remove all content but keep the directory so that
54 | // if you're in it, you don't end up in Trash
55 | rimrafSync(paths.appBuild + '/*');
56 |
57 | // Start the webpack build
58 | build(previousSizeMap);
59 | });
60 |
61 | // Print a detailed summary of build files.
62 | function printFileSizes(stats, previousSizeMap) {
63 | var assets = stats.toJson().assets
64 | .filter(asset => /\.(js|css)$/.test(asset.name))
65 | .map(asset => {
66 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name);
67 | var size = gzipSize(fileContents);
68 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)];
69 | var difference = getDifferenceLabel(size, previousSize);
70 | return {
71 | folder: path.join('build', path.dirname(asset.name)),
72 | name: path.basename(asset.name),
73 | size: size,
74 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '')
75 | };
76 | });
77 | assets.sort((a, b) => b.size - a.size);
78 | var longestSizeLabelLength = Math.max.apply(null,
79 | assets.map(a => stripAnsi(a.sizeLabel).length)
80 | );
81 | assets.forEach(asset => {
82 | var sizeLabel = asset.sizeLabel;
83 | var sizeLength = stripAnsi(sizeLabel).length;
84 | if (sizeLength < longestSizeLabelLength) {
85 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength);
86 | sizeLabel += rightPadding;
87 | }
88 | console.log(
89 | ' ' + sizeLabel +
90 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)
91 | );
92 | });
93 | }
94 |
95 | // Create the production build and print the deployment instructions.
96 | function build(previousSizeMap) {
97 | console.log('Creating an optimized production build...');
98 | webpack(config).run((err, stats) => {
99 | if (err) {
100 | console.error('Failed to create a production build. Reason:');
101 | console.error(err.message || err);
102 | process.exit(1);
103 | }
104 |
105 | console.log(chalk.green('Compiled successfully.'));
106 | console.log();
107 |
108 | console.log('File sizes after gzip:');
109 | console.log();
110 | printFileSizes(stats, previousSizeMap);
111 | console.log();
112 |
113 | var openCommand = process.platform === 'win32' ? 'start' : 'open';
114 | var homepagePath = require(paths.appPackageJson).homepage;
115 | var publicPath = config.output.publicPath;
116 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) {
117 | // "homepage": "http://user.github.io/project"
118 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
119 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
120 | console.log();
121 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
122 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:');
123 | console.log();
124 | console.log(' ' + chalk.cyan('git') + ' commit -am ' + chalk.yellow('"Save local changes"'));
125 | console.log(' ' + chalk.cyan('git') + ' checkout -B gh-pages');
126 | console.log(' ' + chalk.cyan('git') + ' add -f build');
127 | console.log(' ' + chalk.cyan('git') + ' commit -am ' + chalk.yellow('"Rebuild website"'));
128 | console.log(' ' + chalk.cyan('git') + ' filter-branch -f --prune-empty --subdirectory-filter build');
129 | console.log(' ' + chalk.cyan('git') + ' push -f origin gh-pages');
130 | console.log(' ' + chalk.cyan('git') + ' checkout -');
131 | console.log();
132 | } else if (publicPath !== '/') {
133 | // "homepage": "http://mywebsite.com/project"
134 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
135 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
136 | console.log();
137 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
138 | console.log();
139 | } else {
140 | // no homepage or "homepage": "http://mywebsite.com"
141 | console.log('The project was built assuming it is hosted at the server root.');
142 | if (homepagePath) {
143 | // "homepage": "http://mywebsite.com"
144 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
145 | console.log();
146 | } else {
147 | // no homepage
148 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.');
149 | console.log('For example, add this to build it for GitHub Pages:')
150 | console.log();
151 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(','));
152 | console.log();
153 | }
154 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
155 | console.log('You may also serve it locally with a static server:')
156 | console.log();
157 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server');
158 | console.log(' ' + chalk.cyan('pushstate-server') + ' build');
159 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000');
160 | console.log();
161 | }
162 | });
163 | }
164 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var CSSNext = require('postcss-cssnext');
5 | var PostCSSImport = require('postcss-import');
6 | var OfflinePlugin = require('offline-plugin');
7 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
8 | var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeModulesPlugin');
9 | var paths = require('./paths');
10 | var env = require('./env');
11 |
12 | // This is the development configuration.
13 | // It is focused on developer experience and fast rebuilds.
14 | // The production configuration is different and lives in a separate file.
15 | module.exports = {
16 | // This makes the bundle appear split into separate modules in the devtools.
17 | // We don't use source maps here because they can be confusing:
18 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875
19 | // You may want 'cheap-module-source-map' instead if you prefer source maps.
20 | devtool: 'eval',
21 | // These are the "entry points" to our application.
22 | // This means they will be the "root" imports that are included in JS bundle.
23 | // The first two entry points enable "hot" CSS and auto-refreshes for JS.
24 | entry: [
25 | // Include WebpackDevServer client. It connects to WebpackDevServer via
26 | // sockets and waits for recompile notifications. When WebpackDevServer
27 | // recompiles, it sends a message to the client by socket. If only CSS
28 | // was changed, the app reload just the CSS. Otherwise, it will refresh.
29 | // The "?/" bit at the end tells the client to look for the socket at
30 | // the root path, i.e. /sockjs-node/. Otherwise visiting a client-side
31 | // route like /todos/42 would make it wrongly request /todos/42/sockjs-node.
32 | // The socket server is a part of WebpackDevServer which we are using.
33 | // The /sockjs-node/ path I'm referring to is hardcoded in WebpackDevServer.
34 | require.resolve('webpack-dev-server/client') + '?/',
35 | // Include Webpack hot module replacement runtime. Webpack is pretty
36 | // low-level so we need to put all the pieces together. The runtime listens
37 | // to the events received by the client above, and applies updates (such as
38 | // new CSS) to the running application.
39 | require.resolve('webpack/hot/dev-server'),
40 | // We ship a few polyfills by default.
41 | require.resolve('./polyfills'),
42 | // Finally, this is your app's code:
43 | path.join(paths.appSrc, 'index')
44 | // We include the app code last so that if there is a runtime error during
45 | // initialization, it doesn't blow up the WebpackDevServer client, and
46 | // changing JS code would still trigger a refresh.
47 | ],
48 | output: {
49 | // Next line is not used in dev but WebpackDevServer crashes without it:
50 | path: paths.appBuild,
51 | // Add /* filename */ comments to generated require()s in the output.
52 | pathinfo: true,
53 | // This does not produce a real file. It's just the virtual path that is
54 | // served by WebpackDevServer in development. This is the JS bundle
55 | // containing code from all our entry points, and the Webpack runtime.
56 | filename: 'static/js/bundle.js',
57 | // In development, we always serve from the root. This makes config easier.
58 | publicPath: '/'
59 | },
60 | resolve: {
61 | modulesDirectories: [
62 | 'src',
63 | 'node_modules'
64 | ],
65 | // These are the reasonable defaults supported by the Node ecosystem.
66 | extensions: ['.js', '.json', ''],
67 | alias: {
68 | // This `alias` section can be safely removed after ejection.
69 | // We do this because `babel-runtime` may be inside `react-scripts`,
70 | // so when `babel-plugin-transform-runtime` imports it, it will not be
71 | // available to the app directly. This is a temporary solution that lets
72 | // us ship support for generators. However it is far from ideal, and
73 | // if we don't have a good solution, we should just make `babel-runtime`
74 | // a dependency in generated projects.
75 | // See https://github.com/facebookincubator/create-react-app/issues/255
76 | 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator'),
77 | 'react-native': 'react-native-web'
78 | }
79 | },
80 | // Resolve loaders (webpack plugins for CSS, images, transpilation) from the
81 | // directory of `react-scripts` itself rather than the project directory.
82 | // You can remove this after ejecting.
83 | resolveLoader: {
84 | root: paths.ownNodeModules,
85 | moduleTemplates: ['*-loader']
86 | },
87 | module: {
88 | // First, run the linter.
89 | // It's important to do this before Babel processes the JS.
90 | preLoaders: [
91 | {
92 | test: /\.js$/,
93 | loader: 'eslint',
94 | include: paths.appSrc,
95 | }
96 | ],
97 | loaders: [
98 | // Process JS with Babel.
99 | {
100 | test: /\.js$/,
101 | include: paths.appSrc,
102 | loader: 'babel',
103 | query: require('./babel.dev')
104 | },
105 | // "postcss" loader applies autoprefixer to our CSS.
106 | // "css" loader resolves paths in CSS and adds assets as dependencies.
107 | // "style" loader turns CSS into JS modules that inject