Compare commits

..

No commits in common. "master" and "before-refactoring" have entirely different histories.

366 changed files with 97580 additions and 10271 deletions

View File

@ -1,8 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

9
.gitignore vendored
View File

@ -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

View File

@ -1,5 +0,0 @@
.idea
example-project
docs
src/public/js
build

View File

@ -1,11 +0,0 @@
FROM node:14-slim
WORKDIR /usr/src/codecrumbs
COPY package*.json ./
RUN yarn install
COPY . .
EXPOSE 2018 3018

42
LICENSE
View File

@ -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
View File

@ -1,169 +1,2 @@
[![npm version](https://badge.fury.io/js/codecrumbs.svg)](https://badge.fury.io/js/codecrumbs) [![Open Source Love](https://badges.frapsoft.com/os/v2/open-source.svg?v=103)](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

View File

@ -1 +0,0 @@
theme: jekyll-theme-cayman

View File

@ -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 });

View File

@ -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) {}
};

View File

@ -1,3 +0,0 @@
module.exports = {
extends: ['@commitlint/config-conventional']
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@ -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
};

View File

@ -0,0 +1,3 @@
import move from '../zoom/move';
export default () => Promise.resolve({ data: [] });

View File

@ -1 +0,0 @@
//cc:debug#0;step 0

View File

@ -1 +0,0 @@
//cc:debug#2;step 2

View File

@ -1,3 +0,0 @@
//cc:debug#4;step 4
//cc:debug#1;step 1

View File

@ -1 +0,0 @@
//cc:debug#3;step 3

10
example-project/index.js Normal file
View File

@ -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();

View File

@ -1,9 +0,0 @@
#include <iostream>
using namespace std;
//cc:main function
int main()
{
cout << "Hello, World!";
return 0;
}

View File

@ -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();
}
}
}

View File

@ -1,4 +0,0 @@
! cc: main function
program hello
print *, "Hello World!"
end program hello

View File

@ -1,7 +0,0 @@
package main
import "fmt"
// cc:main function
func main() {
fmt.Println("hello world")
}

View File

@ -1,2 +0,0 @@
-- cc:main function
putStrLn "Hello, world!"

View File

@ -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");
}
}

View File

@ -1,4 +0,0 @@
//cc:main function
function greeting() {
console.log('Hello world!');
}

View File

@ -1,5 +0,0 @@
//cc:main function
fun main(args : Array<String>) {
println("Hello, World!")
}

View File

@ -1,2 +0,0 @@
-- hello world program
print ("Hello World!")

View File

@ -1,2 +0,0 @@
/* cc:main function */
let hello = () => "Hello, World!";

View File

@ -1,7 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
# cc: main function.
print "Hello, World!\n";

View File

@ -1,5 +0,0 @@
<?php
//cc:main function
echo "Hello World!";
echo "PHP is so easy!";
?>

View File

@ -1,10 +0,0 @@
import sys
# cc:main function
def main(argv=None):
if argv is None:
argv = sys.argv
print "Hello, world"
return

View File

@ -1,3 +0,0 @@
# cc:main function
puts 'Hello, world!'

View File

@ -1,4 +0,0 @@
//cc:main function
function greeting() {
console.log('Hello world!');
}

View File

@ -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';

View File

@ -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
};
}

View File

@ -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)
);
});
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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()
);

View File

@ -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'
};

View File

@ -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);
}
}

View File

@ -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();

View File

@ -1,2 +0,0 @@
export { firebaseApp, firebaseAuth, firebaseDb } from './firebase';
export { FirebaseList } from './firebase-list';

View File

@ -1,4 +0,0 @@
import createHistory from 'history/createBrowserHistory';
export default createHistory();

View File

@ -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));

View File

@ -1 +0,0 @@
export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION';

View File

@ -1,8 +0,0 @@
import { DISMISS_NOTIFICATION } from './action-types';
export function dismissNotification() {
return {
type: DISMISS_NOTIFICATION
};
}

View File

@ -1,7 +0,0 @@
import * as notificationActions from './actions';
export { notificationActions };
export * from './action-types';
export { notificationReducer } from './reducer';
export { getNotification } from './selectors';

View File

@ -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();
}
}

View File

@ -1,3 +0,0 @@
export function getNotification(state) {
return state.notification;
}

View File

@ -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
});

View File

@ -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;
};

View File

@ -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';

View File

@ -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
};
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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;
}
}
);

View File

@ -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);

View File

@ -1,8 +0,0 @@
import { Record } from 'immutable';
export const Task = new Record({
completed: false,
key: null,
title: null
});

View File

@ -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>

View File

@ -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
);
}

View File

@ -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();
});
}
}

View File

@ -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)
);

View File

@ -1 +0,0 @@
export { default } from './app';

View File

@ -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;

View File

@ -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;
}

View File

@ -1 +0,0 @@
export { default } from './button';

View File

@ -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>
);
}

View File

@ -1 +0,0 @@
export { default } from './github-logo';

View File

@ -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;

View File

@ -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;
}
}

View File

@ -1 +0,0 @@
export { default } from './header';

View File

@ -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;

View File

@ -1 +0,0 @@
export { default } from './icon';

View File

@ -1 +0,0 @@
export { default } from './notification';

View File

@ -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;

View File

@ -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;
}

View File

@ -1 +0,0 @@
export { default } from './require-auth-route';

View File

@ -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;

View File

@ -1 +0,0 @@
export { default } from './require-unauth-route';

View File

@ -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;

View File

@ -1 +0,0 @@
export { default } from './task-filters';

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -1 +0,0 @@
export { default } from './task-form';

View File

@ -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;

View File

@ -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;
}
}

View File

@ -1 +0,0 @@
export { default } from './task-item';

View File

@ -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;

View File

@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More