Compare commits
No commits in common. "master" and "before-refactoring" have entirely different histories.
master
...
before-ref
|
|
@ -1,8 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
|
@ -1,15 +1,6 @@
|
|||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
# Build directories
|
||||
src/public/dist/local/bundle/
|
||||
build/
|
||||
|
||||
# example for dev
|
||||
# example-project
|
||||
|
||||
# Remove some common IDE working directories
|
||||
.idea
|
||||
.DS_STORE
|
||||
|
||||
yarn.lock
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
.idea
|
||||
example-project
|
||||
docs
|
||||
src/public/js
|
||||
build
|
||||
11
Dockerfile
|
|
@ -1,11 +0,0 @@
|
|||
FROM node:14-slim
|
||||
|
||||
WORKDIR /usr/src/codecrumbs
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN yarn install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 2018 3018
|
||||
42
LICENSE
|
|
@ -1,29 +1,21 @@
|
|||
BSD 3-Clause License
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019, Bohdan Liashenko
|
||||
All rights reserved.
|
||||
Copyright (c) 2018 Bohdan Liashenko
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
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:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
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.
|
||||
|
|
|
|||
171
README.md
|
|
@ -1,169 +1,2 @@
|
|||
[](https://badge.fury.io/js/codecrumbs) [](https://github.com/ellerbrock/open-source-badges/) [<img src="https://img.shields.io/twitter/follow/bliashenko.svg?label=Stay%20Tuned&style=social">](https://twitter.com/bliashenko)
|
||||
|
||||
<p align="center">
|
||||
<img src="/docs/logo-sm.png" width="250"/>
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="#what">What</a>
|
||||
<span> · </span>
|
||||
<a href="#demo">Demo</a>
|
||||
<span> · </span>
|
||||
<a href="#get-started">Get started</a>
|
||||
<span> · </span>
|
||||
<a href="#features">Features</a>
|
||||
<span> · </span>
|
||||
<a href="#case-studies">Case studies</a>
|
||||
<span> · </span>
|
||||
<a href="#support">Support</a>
|
||||
</h3>
|
||||
|
||||
**Have you ever got lost in a big or unknown codebase?** This tool will help you to solve that. Also, it will increase your development speed and give more knowledge about your application architecture.
|
||||
> If you like this project, follow me on Twitter [@bliashenko](https://twitter.com/bliashenko) to hear about things I am building.
|
||||
|
||||
## Codecrumbs v2
|
||||
Check out new version of this project as [standalone application](https://codecrumbs.io). Just in a few clicks you can start exploring a codebase in more efficient way, create interactive visual guides and share them with others on your own blog! See [quick guide here](https://codecrumbs.io/guides/web-app-with-github/).
|
||||
|
||||
<p align="center">
|
||||
<a href="https://codecrumbs.io" target="_blank">
|
||||
<img src="https://codecrumbs.io/external/img/common/app-ui-1.png" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
## Demo
|
||||
Check out prepared example for [**standalone version running here**](https://codecrumbs.io/app).
|
||||
|
||||
## Codecrumbs v1
|
||||
|
||||
>**How it works?** You run `codecrumbs` command for a codebase, it analyzes source code and builds its visual representation. Write down a codecrumb-comment and codebase state will be reflected by visual client in browser on the fly.
|
||||
>
|
||||
> Check out [my talk at React-Finland](https://www.youtube.com/watch?v=S_1-1jzLxm4) for more details.
|
||||
|
||||
|
||||
<img src="/docs/main-ui-3.png" width="100%"/>
|
||||
|
||||
## Get started
|
||||
### Install and run
|
||||
>Pre-condition: update/install `NodeJS` version to be >= *8.11.1*
|
||||
|
||||
1) Install ```codecrumbs``` globally (```yarn global add codecrumbs```)
|
||||
2) Run ```codecrumbs -d project-src-dir -e project-src-dir/index.js```. Change parameters to match your project:```-d``` is *directory with source code*, ```-e``` is *entry point file* .
|
||||
3) Go to [http://localhost:2018](http://localhost:2018/#) in the browser to check it out.
|
||||
|
||||
### Configuration
|
||||
Run codecrumbs with CLI params or specify static config file `codecrumbs.config.js` (see example [here](/example-project/codecrumbs.config.js))
|
||||
|
||||
CLI | Config file | Description | Example
|
||||
--- | --- | --- | ---
|
||||
```d``` | ```projectDir``` | Relative path to project source code directory | ```-d src```
|
||||
```e``` | ```entryPoint``` | Relative path to project source entry point file (must be inside ```dir```) | ```-e src/app.js```
|
||||
```x``` | ```excludeDir``` | Relative path(or paths separated by ```,```) to directories for exclusion | ```-x src/doc,src/thirdparty```
|
||||
```p``` | ```clientPort``` | Port for Codecrumbs client (optional, default *2018*) | ```-p 2019```
|
||||
```n``` | ```projectNameAlias``` | Project name alias (optional, default same as ```-d``` value) | ```-n my-hello-world```
|
||||
```C``` | - | Path to codecrumbs.config.js (optional, by default will try to find the file in PWD) | ```-C config/codecrumbs.config.js```
|
||||
```D``` | ```debugModeEnabled``` | Enable debug mode for logs (optional, default is ```false```) | ```-D```
|
||||
|
||||
## Features
|
||||
### Breadcrumbs and trails
|
||||
|
||||
<img src="/docs/cc-ui-3.png" width="750"/>
|
||||
|
||||
UI explained:
|
||||
- enable "Codecrumbs" switch to have codecrumbs tree on the scheme (drop-down contains extra configuration)
|
||||
- choose "current" codecrumbs trail to display (can be either trail or all other "simple" codecrumbs)
|
||||
- select connection between two steps (code for two codecrumbs will be opened in "Sidebar" under "Crumbs" tab)
|
||||
- set other options in dropdowns to configure behaviour of the diagram (show code blocks, details, etc.)
|
||||
|
||||
**How to get there?**
|
||||
|
||||
Leave breadcrumb in code by writing down a comment: ```//cc:[parameters;]```.
|
||||
|
||||
```cc``` (stands for "CodeCrumb") is a prefix which used by the parser; check example of parameters in the table below:
|
||||
|
||||
Example | Description | Use case
|
||||
--- | --- | ---
|
||||
```//cc:remember place``` | simple breadcrumb, ```remember place``` is a title of our first breadcrumb | Mark an important place to not forget where it was
|
||||
```//cc:here is bug;well, seems like a bug in logic``` | simple breadcrumb, ```well, seems like a bug in logic``` is details for breadcrumb, separated by ```;``` | Add extra information, will be rendered in popups
|
||||
```//cc:signin#3;enable route``` | trail of breadcrumbs,```signin``` is the **trail ID**, ```#3``` is order **number of step**, ```enable route``` is a title describing the step. | A sequence of codecrumbs, use to describe some data flow (e.g. user login, or form submit, etc.).
|
||||
```//cc:signin#1;firebase sign in;+2;do call to firebase with credentials``` | trail of breadcrumbs,```+2``` is number of lines to highlight, separated by ```;``` | Use number of lines to highlight the code related to breadcrumb
|
||||
|
||||
> Note: current version supports single line comments only.
|
||||
|
||||
> Hint: you can use trail id without step number (e.g. ```//cc:groupname#;test```) just to group breadcrumbs, you always can add step numbers later when you know the correct order.
|
||||
|
||||
### Multi-codebase integration
|
||||
You might be interested to study connections between several codebases (sub-modules), codecrumbs supports that.
|
||||
Simply start codecrumbs multiple times (once for each codebase), it all **will be synced in one picture** inside the browser tab. To control a diagram UI - select it by clicking on it.
|
||||
|
||||
E.g. for client-server application, go to the source directory for your server code and run `codecrumbs -e your-server-src/index.py -d your-server-src`, same for client `codecrumbs -e src-client/index.js -d src-client`.
|
||||
> **Note:** codebases can be located wherever you want (**no** need to have them like mono-repo, etc.), simply run `codecrumbs` for directory you need.
|
||||
|
||||
<img src="/docs/multi-codebase-cc-2.png" width="100%"/>
|
||||
|
||||
### Multi-language support
|
||||
Current version supports next programming languages:
|
||||
- `C#`
|
||||
- `C++`
|
||||
- `Fortran`
|
||||
- `Go`
|
||||
- `Haskell`
|
||||
- `Java`
|
||||
- `JavaScript`
|
||||
- `Kotlin`
|
||||
- `PHP`
|
||||
- `Python`
|
||||
- `Ruby`
|
||||
- `TypeScript`
|
||||
|
||||
Please file an issue to support other language you would like to have.
|
||||
|
||||
### Dependencies
|
||||
> Note: In current version only [JavaScript, TypeScript] offer this feature
|
||||
|
||||
<img src="/docs/dep-ui-2.png" width="100%"/>
|
||||
|
||||
UI explained:
|
||||
|
||||
- enable "Dependencies" switch
|
||||
- select connection between modules (all involved files will be opened in "Sidebar", so you can see “what is imported” and “its implementation”)
|
||||
|
||||
### Flowchart
|
||||
> Note: In current version only JavaScript offers this feature
|
||||
|
||||
<img src="/docs/flow-ui.png" width="100%"/>
|
||||
|
||||
[js2flowchart](https://github.com/Bogdan-Lyashenko/js-code-to-svg-flowchart) is used in the sidebar to draw flowchart for the selected file code.
|
||||
|
||||
## Support
|
||||
Any support is very much appreciated! 👍 😘 ❤️
|
||||
If you like this project, please, **put a :star: and tweet about it**. Thanks!
|
||||
|
||||
Please, consider [making financial donation](https://opencollective.com/codecrumbs), it will help further development of more cool features! We'll thank you by including your name/company logo here ☺️. Feel free to [ping me](https://www.linkedin.com/in/bohdan-liashenko-bb365854/) for discussion.
|
||||
|
||||
<a href="https://opencollective.com/codecrumbs/donate" target="_blank">
|
||||
<img src="https://opencollective.com/codecrumbs/donate/button@2x.png?color=blue" width=300 />
|
||||
</a>
|
||||
|
||||
#### Sponsors
|
||||
Development supported by [0+X](https://0x.se)
|
||||
|
||||
<a href="https://0x.se" target="_blank">
|
||||
<img src="https://avatars0.githubusercontent.com/u/16350669?s=200&v=4" width=100 />
|
||||
</a>
|
||||
|
||||
#### Backers
|
||||
<a href="https://opencollective.com/codecrumbs/backer/0/website" target="_blank"><img src="https://opencollective.com/codecrumbs/backer/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/codecrumbs/backer/1/website" target="_blank"><img src="https://opencollective.com/codecrumbs/backer/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/codecrumbs/sponsor/0/website" target="_blank"><img src="https://opencollective.com/codecrumbs/sponsor/0/avatar.svg"></a>
|
||||
|
||||
## Contribute
|
||||
When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the [owner](https://github.com/Bogdan-Lyashenko/) of this repository before making a change. Ideas and suggestions are welcome.
|
||||
To start development environment, clone the repo & run:
|
||||
```javascript
|
||||
yarn && yarn start
|
||||
```
|
||||
|
||||
## WIP
|
||||
Next features are developing:
|
||||
- **VS Code extension** - some neat features right inside the code editor. Checkout [the repo here](https://github.com/Bogdan-Lyashenko/vs-code-codecrumbs).
|
||||
# codecrumbs
|
||||
Leave "breadcrumbs" in source code via comments to find your way out from code maze
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
theme: jekyll-theme-cayman
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
const program = require('commander');
|
||||
const colors = require('colors');
|
||||
const _ = require('lodash');
|
||||
|
||||
const showUpdatesInfo = require('./updatesInfo');
|
||||
const server = require('../src/server');
|
||||
|
||||
showUpdatesInfo();
|
||||
|
||||
program
|
||||
.option('-e, --entry [entryPoint]', 'Specify path to entry point file. E.g. `src/app.js`')
|
||||
.option('-d, --dir [projectDir]', 'Specify path to project source code directory. E.g. `src`', '')
|
||||
.option(
|
||||
'-w, --webpack [webpackConfigFile]',
|
||||
'Specify path to webpack config file. E.g. webpack.config.js'
|
||||
)
|
||||
.option(
|
||||
'-t, --tsconfig [tsConfigFile]',
|
||||
'Specify path to typeScript config file. E.g. tsConfig.json'
|
||||
)
|
||||
.option('-p, --port [defaultPort]', 'Specify port for Codecrumbs client. E.g. 3333', 2018)
|
||||
.option('-i, --ideCmd [ideCmd]', 'IDE command to open file')
|
||||
.option('-x, --excludeDir [excludeDirectories]', 'Exclude directories')
|
||||
.option('-n, --projectName [projectNameAlias]', 'Project name alias')
|
||||
.option('-C, --configFile [pathToConfigFile]', 'Path to codecrumbs.config.js')
|
||||
.option('-D, --debugModeEnabled [debugModeEnabled]', 'Enable debug mode for logs.')
|
||||
.parse(process.argv);
|
||||
|
||||
const pathToConfigFile = program.configFile || 'codecrumbs.config.js';
|
||||
const configFileExists = server.checkIfPathExists(pathToConfigFile);
|
||||
if ((!program.entry || !program.dir) && !configFileExists) {
|
||||
console.log(
|
||||
colors.magenta(
|
||||
'Please specify `entryPoint` and `projectDir` params (e.g. `codecrumbs -e src/app.js -d src`). Or use `-C codecrumbs.config.js` instead.'
|
||||
)
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const configFromFile = configFileExists ? require(path.resolve(pathToConfigFile)) : {};
|
||||
|
||||
const configFromCLI = {
|
||||
projectNameAlias: program.projectName,
|
||||
entryPoint: program.entry,
|
||||
projectDir: program.dir,
|
||||
webpackConfigPath: program.webpack,
|
||||
tsConfigPath: program.tsconfig,
|
||||
clientPort: program.port,
|
||||
excludeDir: program.excludeDir,
|
||||
ideCmd: program.ideCmd,
|
||||
debugModeEnabled: program.debugModeEnabled
|
||||
};
|
||||
|
||||
server.setup(_.merge(configFromCLI, configFromFile), { isDev: false });
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
const colors = require('colors');
|
||||
const exec = require('child_process').exec;
|
||||
|
||||
module.exports = () => {
|
||||
try {
|
||||
exec('npm outdated codecrumbs').stdout.on('data', function(data) {
|
||||
const list = data
|
||||
.split(' ')
|
||||
.filter(v => !!v)
|
||||
.map(v => v.trim());
|
||||
|
||||
const latestVersion = list[list.length - 2];
|
||||
console.log(
|
||||
colors.cyan.underline(
|
||||
`There is new version of codecrumbs (${latestVersion}) available! Please update to have all latest features and improvements!`
|
||||
)
|
||||
);
|
||||
});
|
||||
} catch (e) {}
|
||||
};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional']
|
||||
};
|
||||
BIN
docs/cc-ui-3.png
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 93 KiB |
BIN
docs/dev.png
|
Before Width: | Height: | Size: 163 KiB |
BIN
docs/flow-ui.png
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 7.0 MiB |
BIN
docs/logo-sm.png
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
module.exports = {
|
||||
entryPoint: 'example-project/src-client/index.js',
|
||||
projectDir: 'example-project/src-client',
|
||||
clientPort: 1234,
|
||||
projectNameAlias: 'example-project-for-client',
|
||||
debugModeEnabled: true
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import move from '../zoom/move';
|
||||
|
||||
export default () => Promise.resolve({ data: [] });
|
||||
|
|
@ -1 +0,0 @@
|
|||
//cc:debug#0;step 0
|
||||
|
|
@ -1 +0,0 @@
|
|||
//cc:debug#2;step 2
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
//cc:debug#4;step 4
|
||||
|
||||
//cc:debug#1;step 1
|
||||
|
|
@ -1 +0,0 @@
|
|||
//cc:debug#3;step 3
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import tabs from './views/tabs';
|
||||
import dataModel from './dataModel/model';
|
||||
|
||||
const App = {
|
||||
init() {
|
||||
tabs.render(); //cc:render;CallLong line check out tabs.js for more details
|
||||
}
|
||||
};
|
||||
|
||||
App.init();
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#include <iostream>
|
||||
using namespace std;
|
||||
|
||||
//cc:main function
|
||||
int main()
|
||||
{
|
||||
cout << "Hello, World!";
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
//cc:main function
|
||||
using System;
|
||||
namespace HelloWorld
|
||||
{
|
||||
class Hello
|
||||
{
|
||||
static void Main()
|
||||
{
|
||||
Console.WriteLine("Hello World!");
|
||||
|
||||
// Keep the console window open in debug mode.
|
||||
Console.WriteLine("Press any key to exit.");
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
! cc: main function
|
||||
program hello
|
||||
print *, "Hello World!"
|
||||
end program hello
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package main
|
||||
import "fmt"
|
||||
|
||||
// cc:main function
|
||||
func main() {
|
||||
fmt.Println("hello world")
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
-- cc:main function
|
||||
putStrLn "Hello, world!"
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
public class HelloWorld {
|
||||
|
||||
//cc: main function
|
||||
public static void main(String[] args) {
|
||||
// Prints "Hello, World" to the terminal window.
|
||||
System.out.println("Hello, World");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
//cc:main function
|
||||
function greeting() {
|
||||
console.log('Hello world!');
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
//cc:main function
|
||||
|
||||
fun main(args : Array<String>) {
|
||||
println("Hello, World!")
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
-- hello world program
|
||||
print ("Hello World!")
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
/* cc:main function */
|
||||
let hello = () => "Hello, World!";
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
# cc: main function.
|
||||
print "Hello, World!\n";
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
//cc:main function
|
||||
echo "Hello World!";
|
||||
echo "PHP is so easy!";
|
||||
?>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import sys
|
||||
|
||||
# cc:main function
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
print "Hello, world"
|
||||
|
||||
return
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# cc:main function
|
||||
|
||||
puts 'Hello, world!'
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
//cc:main function
|
||||
function greeting() {
|
||||
console.log('Hello world!');
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export const INIT_AUTH = 'INIT_AUTH';
|
||||
|
||||
export const SIGN_IN_ERROR = 'SIGN_IN_ERROR';
|
||||
export const SIGN_IN_SUCCESS = 'SIGN_IN_SUCCESS';
|
||||
|
||||
export const SIGN_OUT_SUCCESS = 'SIGN_OUT_SUCCESS';
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import firebase from 'firebase';
|
||||
import { firebaseAuth } from '../firebase';
|
||||
import {
|
||||
INIT_AUTH,
|
||||
SIGN_IN_ERROR,
|
||||
SIGN_IN_SUCCESS,
|
||||
SIGN_OUT_SUCCESS
|
||||
} from './action-types';
|
||||
|
||||
|
||||
function authenticate(provider) {
|
||||
return dispatch => {
|
||||
//cc:signin#1;firebase sign in;+1;call to firebase with auth provider, proceed if success response
|
||||
firebaseAuth.signInWithPopup(provider)
|
||||
.then(result => dispatch(signInSuccess(result)))
|
||||
.catch(error => dispatch(signInError(error)));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function initAuth(user) {
|
||||
return {
|
||||
type: INIT_AUTH,
|
||||
payload: user
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function signInError(error) {
|
||||
return {
|
||||
type: SIGN_IN_ERROR,
|
||||
payload: error
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function signInSuccess(result) {
|
||||
return {
|
||||
type: SIGN_IN_SUCCESS,
|
||||
payload: result.user
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function signInWithGithub() {
|
||||
return authenticate(new firebase.auth.GithubAuthProvider());
|
||||
}
|
||||
|
||||
|
||||
export function signInWithGoogle() {
|
||||
return authenticate(new firebase.auth.GoogleAuthProvider());
|
||||
}
|
||||
|
||||
|
||||
export function signInWithTwitter() {
|
||||
return authenticate(new firebase.auth.TwitterAuthProvider());
|
||||
}
|
||||
|
||||
|
||||
export function signOut() {
|
||||
return dispatch => {
|
||||
firebaseAuth.signOut()
|
||||
.then(() => dispatch(signOutSuccess()));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function signOutSuccess() {
|
||||
return {
|
||||
type: SIGN_OUT_SUCCESS
|
||||
};
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { firebaseAuth } from '../firebase';
|
||||
import * as authActions from './actions';
|
||||
|
||||
|
||||
export function initAuth(dispatch) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const unsubscribe = firebaseAuth.onAuthStateChanged(
|
||||
authUser => {
|
||||
dispatch(authActions.initAuth(authUser));
|
||||
unsubscribe();
|
||||
resolve();
|
||||
},
|
||||
error => reject(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import * as authActions from './actions';
|
||||
|
||||
|
||||
export { authActions };
|
||||
export * from './action-types';
|
||||
export { initAuth } from './auth';
|
||||
export { authReducer } from './reducer';
|
||||
export { getAuth, isAuthenticated } from './selectors';
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { Record } from 'immutable';
|
||||
import { INIT_AUTH, SIGN_IN_SUCCESS, SIGN_OUT_SUCCESS } from './action-types';
|
||||
|
||||
|
||||
export const AuthState = new Record({
|
||||
authenticated: false,
|
||||
id: null
|
||||
});
|
||||
|
||||
|
||||
export function authReducer(state = new AuthState(), {payload, type}) {
|
||||
switch (type) {
|
||||
case INIT_AUTH:
|
||||
case SIGN_IN_SUCCESS:
|
||||
return state.merge({
|
||||
authenticated: !!payload, //cc:signin#5;toggle 'authenticated' flag
|
||||
id: payload ? payload.uid : null
|
||||
});
|
||||
|
||||
case SIGN_OUT_SUCCESS:
|
||||
return new AuthState();
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
|
||||
export function isAuthenticated(state) {
|
||||
return getAuth(state).authenticated;
|
||||
}
|
||||
|
||||
|
||||
//=====================================
|
||||
// MEMOIZED SELECTORS
|
||||
//-------------------------------------
|
||||
|
||||
export const getAuth = createSelector(
|
||||
state => state.auth,
|
||||
auth => auth.toJS()
|
||||
);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
//cc:firebase config;and some details
|
||||
export const firebaseConfig = {
|
||||
apiKey: 'AIzaSyBsVVpEDrlNPEmshLcmOuE0FxhjPn0AqMg',
|
||||
authDomain: 'todo-react-redux.firebaseapp.com',
|
||||
databaseURL: 'https://todo-react-redux.firebaseio.com',
|
||||
storageBucket: 'firebase-todo-react-redux.appspot.com'
|
||||
};
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { firebaseDb } from './firebase';
|
||||
|
||||
|
||||
export class FirebaseList {
|
||||
constructor(actions, modelClass, path = null) {
|
||||
this._actions = actions;
|
||||
this._modelClass = modelClass;
|
||||
this._path = path;
|
||||
}
|
||||
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
firebaseDb.ref(`${this._path}/${key}`)
|
||||
.set(value, 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)));
|
||||
});
|
||||
|
||||
this._unsubscribe = () => ref.off();
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
unwrapSnapshot(snapshot) {
|
||||
let attrs = snapshot.val();
|
||||
attrs.key = snapshot.key;
|
||||
return new this._modelClass(attrs);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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();
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { firebaseApp, firebaseAuth, firebaseDb } from './firebase';
|
||||
export { FirebaseList } from './firebase-list';
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import createHistory from 'history/createBrowserHistory';
|
||||
|
||||
|
||||
export default createHistory();
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
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 registerServiceWorker from './utils/register-service-worker';
|
||||
import App from './views/app';
|
||||
|
||||
|
||||
const store = configureStore();
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
//cc:layout#0;start
|
||||
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));
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION';
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { DISMISS_NOTIFICATION } from './action-types';
|
||||
|
||||
|
||||
export function dismissNotification() {
|
||||
return {
|
||||
type: DISMISS_NOTIFICATION
|
||||
};
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import * as notificationActions from './actions';
|
||||
|
||||
|
||||
export { notificationActions };
|
||||
export * from './action-types';
|
||||
export { notificationReducer } from './reducer';
|
||||
export { getNotification } from './selectors';
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { Record } from 'immutable';
|
||||
import { REMOVE_TASK_SUCCESS } from '../tasks';
|
||||
import { DISMISS_NOTIFICATION } from './action-types';
|
||||
|
||||
|
||||
export const NotificationState = new Record({
|
||||
actionLabel: '',
|
||||
display: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
|
||||
export function notificationReducer(state = new NotificationState(), action) {
|
||||
switch (action.type) {
|
||||
case REMOVE_TASK_SUCCESS:
|
||||
return state.merge({
|
||||
actionLabel: 'Undo',
|
||||
display: true,
|
||||
message: 'Task deleted'
|
||||
});
|
||||
|
||||
case DISMISS_NOTIFICATION:
|
||||
return new NotificationState();
|
||||
|
||||
default:
|
||||
return new NotificationState();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export function getNotification(state) {
|
||||
return state.notification;
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { routerReducer } from 'react-router-redux';
|
||||
import { combineReducers } from 'redux';
|
||||
import { authReducer } from './auth';
|
||||
import { notificationReducer } from './notification';
|
||||
import { tasksReducer } from './tasks';
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
auth: authReducer,
|
||||
notification: notificationReducer,
|
||||
routing: routerReducer,
|
||||
tasks: tasksReducer
|
||||
});
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { routerMiddleware } from 'react-router-redux';
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import history from './history';
|
||||
import reducers from './reducers';
|
||||
|
||||
|
||||
export default (initialState = {}) => {
|
||||
let middleware = applyMiddleware(thunk, routerMiddleware(history));
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const devToolsExtension = window.devToolsExtension;
|
||||
if (typeof devToolsExtension === 'function') {
|
||||
middleware = compose(middleware, devToolsExtension());
|
||||
}
|
||||
}
|
||||
|
||||
const store = createStore(reducers, initialState, middleware);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./reducers', () => {
|
||||
store.replaceReducer(require('./reducers').default);
|
||||
});
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
export const CREATE_TASK_ERROR = 'CREATE_TASK_ERROR';
|
||||
export const CREATE_TASK_SUCCESS = 'CREATE_TASK_SUCCESS';
|
||||
|
||||
export const REMOVE_TASK_ERROR = 'REMOVE_TASK_ERROR';
|
||||
export const REMOVE_TASK_SUCCESS = 'REMOVE_TASK_SUCCESS';
|
||||
|
||||
export const UNDELETE_TASK_ERROR = 'UNDELETE_TASK_ERROR';
|
||||
|
||||
export const UPDATE_TASK_ERROR = 'UPDATE_TASK_ERROR';
|
||||
export const UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS';
|
||||
|
||||
export const FILTER_TASKS = 'FILTER_TASKS';
|
||||
export const LOAD_TASKS_SUCCESS = 'LOAD_TASKS_SUCCESS';
|
||||
export const UNLOAD_TASKS_SUCCESS = 'UNLOAD_TASKS_SUCCESS';
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import { getDeletedTask } from './selectors';
|
||||
import { taskList } from './task-list';
|
||||
import {
|
||||
CREATE_TASK_ERROR,
|
||||
CREATE_TASK_SUCCESS,
|
||||
REMOVE_TASK_ERROR,
|
||||
REMOVE_TASK_SUCCESS,
|
||||
FILTER_TASKS,
|
||||
LOAD_TASKS_SUCCESS,
|
||||
UNDELETE_TASK_ERROR,
|
||||
UNLOAD_TASKS_SUCCESS,
|
||||
UPDATE_TASK_ERROR,
|
||||
UPDATE_TASK_SUCCESS
|
||||
} from './action-types';
|
||||
|
||||
|
||||
export function createTask(title) {
|
||||
return dispatch => {
|
||||
taskList.push({completed: false, title})
|
||||
.catch(error => dispatch(createTaskError(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function createTaskError(error) {
|
||||
return {
|
||||
type: CREATE_TASK_ERROR,
|
||||
payload: error
|
||||
};
|
||||
}
|
||||
|
||||
export function createTaskSuccess(task) {
|
||||
return {
|
||||
type: CREATE_TASK_SUCCESS,
|
||||
payload: task
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTask(task) {
|
||||
return dispatch => {
|
||||
taskList.remove(task.key)
|
||||
.catch(error => dispatch(removeTaskError(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTaskError(error) {
|
||||
return {
|
||||
type: REMOVE_TASK_ERROR,
|
||||
payload: error
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTaskSuccess(task) {
|
||||
return {
|
||||
type: REMOVE_TASK_SUCCESS,
|
||||
payload: task
|
||||
};
|
||||
}
|
||||
|
||||
export function undeleteTask() {
|
||||
return (dispatch, getState) => {
|
||||
const task = getDeletedTask(getState());
|
||||
if (task) {
|
||||
taskList.set(task.key, {completed: task.completed, title: task.title})
|
||||
.catch(error => dispatch(undeleteTaskError(error)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function undeleteTaskError(error) {
|
||||
return {
|
||||
type: UNDELETE_TASK_ERROR,
|
||||
payload: error
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTaskError(error) {
|
||||
return {
|
||||
type: UPDATE_TASK_ERROR,
|
||||
payload: error
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTask(task, changes) {
|
||||
return dispatch => {
|
||||
taskList.update(task.key, changes)
|
||||
.catch(error => dispatch(updateTaskError(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function updateTaskSuccess(task) {
|
||||
return {
|
||||
type: UPDATE_TASK_SUCCESS,
|
||||
payload: task
|
||||
};
|
||||
}
|
||||
|
||||
export function loadTasksSuccess(tasks) {
|
||||
return {
|
||||
type: LOAD_TASKS_SUCCESS,
|
||||
payload: tasks
|
||||
};
|
||||
}
|
||||
|
||||
export function filterTasks(filterType) {
|
||||
return {
|
||||
type: FILTER_TASKS,
|
||||
payload: {filterType}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadTasks() {
|
||||
return (dispatch, getState) => {
|
||||
const { auth } = getState();
|
||||
taskList.path = `tasks/${auth.id}`;
|
||||
taskList.subscribe(dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
export function unloadTasks() {
|
||||
taskList.unsubscribe();
|
||||
return {
|
||||
type: UNLOAD_TASKS_SUCCESS
|
||||
};
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import * as tasksActions from './actions';
|
||||
|
||||
|
||||
export { tasksActions };
|
||||
export * from './action-types';
|
||||
export { tasksReducer } from './reducer';
|
||||
export { getTaskFilter, getVisibleTasks } from './selectors';
|
||||
export { Task } from './task';
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import { List, Record } from 'immutable';
|
||||
import { SIGN_OUT_SUCCESS } from '../auth/action-types';
|
||||
import {
|
||||
CREATE_TASK_SUCCESS,
|
||||
REMOVE_TASK_SUCCESS,
|
||||
FILTER_TASKS,
|
||||
LOAD_TASKS_SUCCESS,
|
||||
UPDATE_TASK_SUCCESS
|
||||
} from './action-types';
|
||||
|
||||
|
||||
export const TasksState = new Record({
|
||||
deleted: null,
|
||||
filter: '',
|
||||
list: new List(),
|
||||
previous: null
|
||||
});
|
||||
|
||||
|
||||
export function tasksReducer(state = new TasksState(), {payload, type}) {
|
||||
switch (type) {
|
||||
case CREATE_TASK_SUCCESS:
|
||||
return state.merge({
|
||||
deleted: null,
|
||||
previous: null,
|
||||
list: state.deleted && state.deleted.key === payload.key ?
|
||||
state.previous :
|
||||
state.list.unshift(payload)
|
||||
});
|
||||
|
||||
case REMOVE_TASK_SUCCESS:
|
||||
return state.merge({
|
||||
deleted: payload,
|
||||
previous: state.list,
|
||||
list: state.list.filter(task => task.key !== payload.key)
|
||||
});
|
||||
|
||||
case FILTER_TASKS:
|
||||
return state.set('filter', payload.filterType || '');
|
||||
|
||||
case LOAD_TASKS_SUCCESS:
|
||||
return state.set('list', new List(payload.reverse()));
|
||||
|
||||
case UPDATE_TASK_SUCCESS:
|
||||
return state.merge({
|
||||
deleted: null,
|
||||
previous: null,
|
||||
list: state.list.map(task => {
|
||||
return task.key === payload.key ? payload : task;
|
||||
})
|
||||
});
|
||||
|
||||
case SIGN_OUT_SUCCESS:
|
||||
return new TasksState();
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
|
||||
export function getTasks(state) {
|
||||
return state.tasks;
|
||||
}
|
||||
|
||||
export function getTaskList(state) {
|
||||
return getTasks(state).list;
|
||||
}
|
||||
|
||||
export function getTaskFilter(state) {
|
||||
return getTasks(state).filter;
|
||||
}
|
||||
|
||||
export function getDeletedTask(state) {
|
||||
return getTasks(state).deleted;
|
||||
}
|
||||
|
||||
|
||||
//=====================================
|
||||
// MEMOIZED SELECTORS
|
||||
//-------------------------------------
|
||||
|
||||
export const getVisibleTasks = createSelector(
|
||||
getTaskList,
|
||||
getTaskFilter,
|
||||
(tasks, filter) => {
|
||||
switch (filter) {
|
||||
case 'active':
|
||||
return tasks.filter(task => !task.completed);
|
||||
|
||||
case 'completed':
|
||||
return tasks.filter(task => task.completed);
|
||||
|
||||
default:
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { FirebaseList } from '../firebase';
|
||||
import * as taskActions from './actions';
|
||||
import { Task } from './task';
|
||||
|
||||
|
||||
export const taskList = new FirebaseList({
|
||||
onAdd: taskActions.createTaskSuccess,
|
||||
onChange: taskActions.updateTaskSuccess,
|
||||
onLoad: taskActions.loadTasksSuccess,
|
||||
onRemove: taskActions.removeTaskSuccess
|
||||
}, Task);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { Record } from 'immutable';
|
||||
|
||||
|
||||
export const Task = new Record({
|
||||
completed: false,
|
||||
key: null,
|
||||
title: null
|
||||
});
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
|
||||
<template>
|
||||
<p>{{ greeting }} World!</p>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
data: function () {
|
||||
return {
|
||||
greeting: 'Hello'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
//cc:test vue
|
||||
<style scoped>
|
||||
p {
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import { findRenderedComponentWithType, renderIntoDocument } from 'react-dom/test-utils';
|
||||
|
||||
|
||||
export function createTestComponent(TestComponent, props) {
|
||||
return findRenderedComponentWithType(
|
||||
renderIntoDocument(<TestComponent {...props}/>),
|
||||
TestComponent
|
||||
);
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
// 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() {
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) return;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
if (!isLocalhost) {
|
||||
registerValidSW(swUrl);
|
||||
} else {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { authActions, getAuth } from '../../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';
|
||||
|
||||
//cc:layout#1;describe pages;some details long description for separare popup
|
||||
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
|
||||
};
|
||||
|
||||
//cc:here
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App)
|
||||
);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './app';
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './button';
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './github-logo';
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
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 React Redux</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-react-redux">
|
||||
<GitHubLogo />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
Header.propTypes = {
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
signOut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
export default Header;
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
@import 'views/styles/shared';
|
||||
|
||||
|
||||
.header {
|
||||
padding: 10px 0;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
float: left;
|
||||
font-size: rem(14px);
|
||||
font-weight: 400;
|
||||
line-height: 40px;
|
||||
text-rendering: auto;
|
||||
transform: translate(0,0);
|
||||
|
||||
&:before {
|
||||
display: inline-block;
|
||||
border: 2px solid #eee;
|
||||
margin-right: 8px;
|
||||
border-radius: 100%;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './header';
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './icon';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './notification';
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './notification.css';
|
||||
|
||||
|
||||
class Notification extends Component {
|
||||
static propTypes = {
|
||||
action: PropTypes.func.isRequired,
|
||||
actionLabel: PropTypes.string.isRequired,
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
display: PropTypes.bool.isRequired,
|
||||
duration: PropTypes.number,
|
||||
message: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.display) {
|
||||
this.startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
clearTimer() {
|
||||
if (this.timerId) {
|
||||
clearTimeout(this.timerId);
|
||||
}
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
this.clearTimer();
|
||||
this.timerId = setTimeout(() => {
|
||||
this.props.dismiss();
|
||||
}, this.props.duration || 5000);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="notification">
|
||||
<p className="notification__message" ref={c => this.message = c}>{this.props.message}</p>
|
||||
<button
|
||||
className="btn notification__button"
|
||||
onClick={this.props.action}
|
||||
ref={c => this.button = c}
|
||||
type="button">{this.props.actionLabel}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Notification;
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
@import 'views/styles/shared';
|
||||
|
||||
|
||||
.notification {
|
||||
@include clearfix;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 60px;
|
||||
margin-left: -100px;
|
||||
border: 1px solid #aaa;
|
||||
padding: 10px 15px;
|
||||
width: 200px;
|
||||
font-size: rem(16px);
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.notification__message {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.notification__button {
|
||||
float: right;
|
||||
font-size: rem(16px);
|
||||
line-height: 24px;
|
||||
text-transform: uppercase;
|
||||
color: #85bf6b;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './require-auth-route';
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Route, Redirect } from 'react-router-dom'
|
||||
|
||||
//cc:signin#6;enable route;details
|
||||
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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './require-unauth-route';
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './task-filters';
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './task-form';
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
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?"
|
||||
ref={e => this.titleInput = e}
|
||||
type="text"
|
||||
value={this.state.title}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default TaskForm;
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './task-item';
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
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';
|
||||
|
||||
//cc:there is task;extra
|
||||
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;
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||