├── .DS_Store
├── .babelrc
├── .gitignore
├── README.md
├── app
├── .DS_Store
├── App.js
├── api
│ ├── Firebase.js
│ ├── auth
│ │ ├── index.js
│ │ └── services
│ │ │ └── AuthFirebase.js
│ └── post
│ │ ├── index.js
│ │ └── services
│ │ └── PostFirebase.js
├── components
│ ├── form
│ │ ├── Editor.js
│ │ ├── Editors
│ │ │ ├── EditorBlank.js
│ │ │ ├── EditorHashtag.js
│ │ │ ├── EditorLink.js
│ │ │ ├── EditorMedia.js
│ │ │ ├── EditorRich.js
│ │ │ ├── EditorRichWithTab.js
│ │ │ └── index.js
│ │ ├── InputText.js
│ │ ├── Loading.js
│ │ ├── Textarea.js
│ │ ├── ValidateWrapControl.js
│ │ ├── WrapContainer.js
│ │ ├── WrapTransitions.js
│ │ └── index.js
│ ├── layouts
│ │ └── master
│ │ │ ├── AppMaster.js
│ │ │ └── partials
│ │ │ ├── Header.js
│ │ │ └── Logo.js
│ └── pages
│ │ ├── member
│ │ ├── Login.js
│ │ ├── Profile.js
│ │ └── Register.js
│ │ └── posts
│ │ ├── PostForm.js
│ │ ├── PostView.js
│ │ ├── PostsList.js
│ │ ├── index.js
│ │ └── partials
│ │ └── PostItem.js
├── configs
│ └── index.js
├── redux
│ ├── Root.js
│ ├── actions
│ │ ├── AuthAction.js
│ │ ├── AwaitAction.js
│ │ └── PostAction.js
│ ├── containers
│ │ ├── CompareUID.js
│ │ ├── member
│ │ │ ├── LoginContainer.js
│ │ │ ├── LogoutContainer.js
│ │ │ ├── ProfileContainer.js
│ │ │ ├── RegisterContainer.js
│ │ │ └── index.js
│ │ ├── posts
│ │ │ ├── CreatePostContainer.js
│ │ │ ├── EditPostContainer.js
│ │ │ ├── PostViewContainer.js
│ │ │ ├── PostsListContainer.js
│ │ │ └── index.js
│ │ └── requireAuth.js
│ ├── reducers
│ │ ├── auth.js
│ │ ├── await.js
│ │ ├── index.js
│ │ └── posts.js
│ └── store
│ │ └── configureStore.js
├── routes
│ └── index.js
├── stylesheets
│ ├── _mixins.scss
│ ├── _variables.scss
│ ├── pages
│ │ └── post_lists.scss
│ ├── partials
│ │ ├── buttons.scss
│ │ ├── header.scss
│ │ ├── loading.scss
│ │ ├── margin.scss
│ │ └── text.scss
│ ├── richText.scss
│ └── style.scss
├── test
│ └── Test.js
└── utils
│ ├── firebaseUtils.js
│ ├── index.js
│ └── reduxAwait.js
├── config.js
├── configs
└── webpack_dev.js
├── firebase.json
├── package.json
├── public
├── 0908fb27a6a6f3a2026742aa34a81b7f.svg
├── 30ba5cd4a3ad1d61e85a4847eb174dd2.woff2
├── 404a525502f8e5ba7e93b9f02d9e83a9.eot
├── 448c34a56d699c29117adc64c43affeb.woff2
├── 4658299167a2c591b4abac2f9a2a1476.ttf
├── 891e3f340c1126b4c7c142e5f6e86816.woff
├── 89889688147bd7575d6327160d64e760.svg
├── 926c93d201fe51c8f351e858468980c3.woff2
├── b0a05c8de588b1af413ed5b07d355803.woff
├── bae4a87c1e5dff40baa3f49d52f5347a.svg
├── bundle.js
├── c2cf0100e8770e3d4019a2edaa39adaa.eot
├── e18bbf611f2a2e43afc071aa2f4e1512.ttf
├── f4769f9bdb7466be65088239c12046d1.eot
├── fa2772327f55d8198301fdb8bcfc8158.woff
├── fb650aaf10736ffb9c4173079616bf01.ttf
├── firebase.json
├── images
│ └── logo.png
└── index.html
└── webpack.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["stage-0","es2015","react"],
3 | "env": {
4 | "development": {
5 | "plugins": [["react-transform", {
6 | "transforms": [{
7 | "transform": "react-transform-hmr",
8 | // if you use React Native, pass "react-native" instead:
9 | "imports": ["react"],
10 | // this is important for Webpack HMR:
11 | "locals": ["module"]
12 | }]
13 | // note: you can put more transforms into array
14 | // this is just one of them!
15 | }]]
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
3 | .firebase.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Redux + Firebase = Blog Example + Authenticated
2 | This project is a simple blog using React, Redux & Firebase.
3 | * Register and Login
4 | * Create and Edit Post
5 | * View Posts List
6 | * View Post
7 |
8 | ## Demo online
9 | [http://react-redux-blog.firebaseapp.com/](http://react-redux-blog.firebaseapp.com/)
10 | Demo:
11 |
12 | ![demo gif]
13 | [demo]
14 | [demo]: https://media.giphy.com/media/l396NAqYcOiPewcs8/giphy.gif
15 |
16 | ## Starter kit
17 | * [React redux starter kit](http://github.com/thanhtungdp/redux-500)
18 |
19 |
20 | ## About
21 | Using technologies:
22 | * [React](https://github.com/facebook/react)
23 | * [React Router](https://github.com/rackt/react-router)
24 | * [React Hot Loader](https://github.com/gaearon/react-hot-loader)
25 | * [Babel](http://babeljs.io) for ES6 and ES7 magic
26 | * [Webpack](http://webpack.github.io) for bundling
27 | * [style-loader](https://github.com/webpack/style-loader), [sass-loader](https://github.com/jtangelder/sass-loader) and [less-loader](https://github.com/webpack/less-loader) to allow import of stylesheets in plain css, sass and less,
28 | * [Redux](https://github.com/rackt/redux)'s futuristic [Flux](https://facebook.github.io/react/blog/2014/05/06/flux.html) implementation
29 | * [Firebase](https://www.npmjs.com/package/firebase) - Database
30 | * [Redux Dev Tools Extensions](https://github.com/zalmoxisus/redux-devtools-extension) for next generation DX (developer experience).
31 | * [React redux starter kit](http://github.com/thanhtungdp/redux-500)
32 | * [Redux-await](https://github.com/kolodny/redux-await) - Manage async redux actions sanely
33 | * [Redux Form](http://redux-form.com) Form validate
34 | * [Draft.js](draftjs.org) Text Editor
35 | * [React-bootstrap](https://react-bootstrap.github.io/) Bootstrap component
36 | * [React Masonry](https://github.com/eiriklv/react-masonry-component)
37 | * [Bootstrap-sass](https://github.com/twbs/bootstrap-sass) import scss to app
38 | * [FontAwesome](fortawesome.github.io/Font-Awesome/icons/) font icons
39 | * [SimpleLineIcons](http://thesabbir.github.io/simple-line-icons/) font icons
40 | * [Animate.css](https://daneden.github.io/animate.css/) best for transitions
41 |
42 | ## Installation
43 | ``` code
44 | git clone https://github.com/thanhtungdp/react-redux-firebase-blog blog
45 | cd blog
46 | npm install
47 | ```
48 |
49 | ## Running dev server
50 | ``` code
51 | npm run dev
52 | (or npm run start)
53 | ```
54 | Default server is running on port `8080`. If you want to change, you can edit at `configs/webpack_dev.js`.
55 | Go to: `http://localhost:8080`
56 |
57 | ## Building production
58 | ``` code
59 | npm run buid
60 | ```
61 | It generates static file `public/bundle.js`.
62 |
63 | You can run `public/index.html`
64 |
65 | ## Directory structure
66 | ```
67 | Root
68 | ├── app
69 | │ ├── api
70 | │ ├── components
71 | | ├── redux
72 | | | ├── actions
73 | | | ├── reducers
74 | | | ├── actions
75 | | | ├── containers
76 | | | ├── Root.js
77 | │ ├── routes
78 | │ ├── stylesheets
79 | │ ├── views
80 | │ ├── App.js
81 | ├── configs
82 | | ├── webpack_dev.js
83 | ├── public
84 | | ├── index.html
85 | ├── package.json
86 | ├── server.js
87 | └── webpack.config.js
88 | ```
89 |
90 | ## Import other sass packages
91 | Edit at `app/App.js`
92 | Example:
93 | ``` javascript
94 | import '../node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss';
95 | import '../node_modules/font-awesome/scss/font-awesome.scss'
96 | import './stylesheets/style.scss';
97 | ```
98 | ## Using with react router
99 | You can edit at `app/routes/index.js`
100 | ``` javascript
101 | // Import react
102 | import React from 'react';
103 | import {Router, Route, IndexRoute, hashHistory} from 'react-router';
104 | import { syncHistoryWithStore, routerReducer } from 'react-router-redux';
105 |
106 | // Import components
107 | import AppMaster from '../views/AppMaster';
108 | import SearchAppContainer from '../redux/containers/SearchAppContainer';
109 |
110 | export default () => {
111 | return (
112 |
113 |
114 |
115 |
116 | )
117 | }
118 | ```
119 | ## Documents
120 | * [Awesome Redux](https://github.com/xgrommx/awesome-redux)
121 | * [Redux offical docs](http://redux.js.org/)
122 | * [Redux example es6](https://github.com/yildizberkay/redux-example)
123 | * [Firebase Docs](https://www.firebase.com/docs/web/api/)
124 | * [Redux Blog Example + Nodejs Server](https://medium.com/@rajaraodv/a-guide-for-building-a-react-redux-crud-app-7fe0b8943d0f#.vke00op6b)
125 |
126 | ## More documents
127 | Is updating ...
128 |
--------------------------------------------------------------------------------
/app/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/app/.DS_Store
--------------------------------------------------------------------------------
/app/App.js:
--------------------------------------------------------------------------------
1 | /* React */
2 | import React,{Component} from 'react';
3 | import {render} from 'react-dom';
4 |
5 | /* Root provider from redux */
6 | import Root from './redux/Root';
7 |
8 | /* Stylesheet */
9 | import 'bootstrap-sass/assets/stylesheets/_bootstrap.scss';
10 | import 'font-awesome/scss/font-awesome.scss';
11 | import 'simple-line-icons/scss/simple-line-icons.scss';
12 | import 'animate.css/animate.css';
13 | import './stylesheets/style.scss';
14 |
15 | /* Test */
16 | import './test/Test.js';
17 |
18 | render(, document.getElementById('root'))
--------------------------------------------------------------------------------
/app/api/Firebase.js:
--------------------------------------------------------------------------------
1 | import Firebase from 'firebase';
2 | export default new Firebase('http://vndev-chat.firebaseio.com');
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/api/auth/index.js:
--------------------------------------------------------------------------------
1 | import AuthFirebase from './services/AuthFirebase';
2 |
3 | export class Auth {
4 | constructor() {
5 | this.service = new AuthFirebase(this);
6 | return this.service;
7 | }
8 | }
9 |
10 | export default new Auth()
--------------------------------------------------------------------------------
/app/api/auth/services/AuthFirebase.js:
--------------------------------------------------------------------------------
1 | import firebase from '../../Firebase';
2 |
3 | export default class AuthFirebase {
4 | constructor(context) {
5 | this.context = context;
6 | this.user = {};
7 | this.profile = {};
8 | }
9 |
10 | /**
11 | * Register user
12 | * @param email | string
13 | * @param password | string
14 | * @returns {Promise}
15 | */
16 | register(email, password, profile = {}) {
17 | let context = this;
18 | let promise = new Promise(function (resolve, reject) {
19 | firebase.createUser({email, password}, function (err, user) {
20 | if (err) {
21 | reject(err.message);
22 | }
23 | else {
24 | context.updateProfile(profile, user.uid).then((profile)=> {
25 | resolve({email, password});
26 | });
27 | }
28 | });
29 | });
30 | return promise;
31 | }
32 |
33 | /**
34 | * Clean user from requesst
35 | * @param user
36 | * @returns {{email: *, token: (*|token|{isFetching}|{token, isFetching, error}|string), uid: (*|number)}}
37 | */
38 | cleanUser(user) {
39 | return {
40 | email: user.password.email,
41 | token: user.token,
42 | uid: user.uid
43 | }
44 | }
45 |
46 | /**
47 | * Login
48 | * @param email | string
49 | * @param password | password
50 | * @returns {Promise}
51 | */
52 | login(email, password) {
53 | let promise = new Promise(function (resolve, reject) {
54 | firebase.authWithPassword({email: email, password: password}, function (err, user) {
55 | if (err) {
56 | reject(err.message);
57 | }
58 | else {
59 | resolve(this.cleanUser(user));
60 | }
61 | }.bind(this));
62 | }.bind(this));
63 | return promise;
64 | }
65 |
66 | /**
67 | * Check is Authenticated
68 | * @returns {Promise}
69 | */
70 | isAuthenticated() {
71 | let user = firebase.getAuth();
72 | let promise = new Promise(function (resolve, reject) {
73 | if (user) {
74 | resolve(this.cleanUser(user));
75 | }
76 | else reject('not auth token');
77 | }.bind(this));
78 | return promise;
79 | }
80 |
81 | /**
82 | * Logout
83 | * @returns {Promise}
84 | */
85 | logout() {
86 | return new Promise(function (relsove, reject) {
87 | firebase.unauth();
88 | relsove();
89 | })
90 | }
91 |
92 | /**
93 | * Update profile
94 | * @param profile | object
95 | * @param uid | string (eg: user_id)
96 | * @returns {Promise}
97 | */
98 | updateProfile(profile = {}, uid) {
99 | let userProfile = firebase.child('users').child(uid);
100 | let promise = new Promise(function (resolve, reject) {
101 | userProfile.set(profile, function (err) {
102 | if (err) {
103 | reject(err);
104 | }
105 | else {
106 | resolve(profile);
107 | }
108 | });
109 | });
110 | return promise;
111 | }
112 |
113 | /**
114 | * Get profile
115 | * @param uid
116 | * @returns {Promise}
117 | */
118 | getProfile(uid) {
119 | let userProfile = firebase.child('users').child(uid);
120 | let promise = new Promise(function (resolve, reject) {
121 | userProfile.on("value", (snapshot)=> {
122 | let profile = snapshot.val();
123 | resolve(profile);
124 | }, (error)=> {
125 | reject(error);
126 | });
127 | });
128 | return promise;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/app/api/post/index.js:
--------------------------------------------------------------------------------
1 | import PostFirebase from './services/PostFirebase';
2 |
3 | export class Post {
4 | constructor() {
5 | this.service = new PostFirebase(this);
6 | return this.service;
7 | }
8 | }
9 |
10 | export default new Post();
--------------------------------------------------------------------------------
/app/api/post/services/PostFirebase.js:
--------------------------------------------------------------------------------
1 | import firebase from '../../Firebase';
2 | import Auth from '../../auth/index';
3 |
4 | import {mergeArrayObjectWithKey, mergeObjectWithKey} from '../../../utils/firebaseUtils';
5 |
6 | export default class PostFirebase {
7 |
8 | /**
9 | * Create new post
10 | * @param newPost | object
11 | * @param uid | string (eg: user_id)
12 | * @returns {Promise}
13 | */
14 | createPost(newPost, uid) {
15 | let postFirebase = firebase.child('posts');
16 | const post = Object.assign(newPost, {
17 | uid: uid,
18 | created_at: new Date().getTime()
19 | });
20 | let promise = new Promise((resolve, reject)=> {
21 | try {
22 | let create = postFirebase.push();
23 | let key = create.key();
24 | create.set(post, function () {
25 | post.id = key;
26 | resolve(post);
27 | });
28 | }
29 | catch (e) {
30 | reject(e.message);
31 | }
32 | })
33 | return promise;
34 | }
35 |
36 | /**
37 | * Update post
38 | * @param post | object
39 | * @param post_id | string
40 | * @returns {Promise}
41 | */
42 | updatePost(post, post_id) {
43 | let postFirebase = firebase.child('posts').child(post_id);
44 | post.created_at = new Date().getTime();
45 |
46 | let promise = new Promise((resolve, reject)=> {
47 | try {
48 | postFirebase.update(post, function () {
49 | post.id = post_id;
50 | resolve(post);
51 | });
52 | }
53 | catch (e) {
54 | reject(e.message);
55 | }
56 | })
57 | return promise;
58 | }
59 |
60 | /**
61 | * Get post lists
62 | * @returns {Promise}
63 | */
64 | getPostsList() {
65 | let postsFirebase = firebase.child('posts');
66 | let promise = new Promise((resolve, reject) => {
67 | try {
68 | postsFirebase.on('value', function (snapshot) {
69 | let posts = mergeArrayObjectWithKey(snapshot.val());
70 | let loopGetUser = (postIndex) => {
71 | if (postIndex == posts.length) {
72 | resolve(posts.reverse())
73 | } else {
74 | Auth.getProfile(posts[postIndex].uid).then((profile)=> {
75 | posts[postIndex].user = profile;
76 | loopGetUser(postIndex + 1)
77 | });
78 | }
79 | }
80 | loopGetUser(0);
81 | });
82 | }
83 | catch (err) {
84 | reject(err.message);
85 | }
86 | });
87 | return promise;
88 | }
89 |
90 | /**
91 | * Get Post
92 | * @param id | string
93 | * @returns {Promise}
94 | */
95 | getPost(id) {
96 | let postFirebase = firebase.child('posts').child(id);
97 | let promise = new Promise((resolve, reject) => {
98 | try {
99 | postFirebase.on('value', function (snapshot) {
100 | let post = snapshot.val();
101 | if (post) {
102 | Auth.getProfile(post.uid).then((profile)=> {
103 | post.user = profile;
104 | resolve(mergeObjectWithKey(post, id));
105 | });
106 | }
107 | else {
108 | reject('Post\'s not exists');
109 | }
110 | });
111 | }
112 | catch (e) {
113 | reject(e.message);
114 | }
115 | });
116 | return promise;
117 | }
118 |
119 | deletePost(id) {
120 | let postFirebase = firebase.child('posts').child(id);
121 | return new Promise((resolve, reject) => {
122 | postFirebase.remove((err)=> {
123 | if (err) {
124 | reject(err.message);
125 | }
126 | else resolve('Post was deleted');
127 | });
128 | })
129 | }
130 | }
--------------------------------------------------------------------------------
/app/components/form/Editor.js:
--------------------------------------------------------------------------------
1 | import React,{Component, PropTypes} from 'react';
2 | import {FormGroup, ControlLabel, FormControl} from 'react-bootstrap';
3 | import ValidateWrapControl from './ValidateWrapControl';
4 | import EditorRich from './Editor/index';
5 | import validator from 'validator';
6 |
7 | export default class EditorRich extends Component {
8 | constructor(props) {
9 | super(props);
10 | }
11 |
12 | render() {
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/app/components/form/Editors/EditorBlank.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {
4 | Editor, EditorState, ContentState, RichUtils,
5 | getDefaultKeyBinding, KeyBindingUtil,
6 | Entity, convertToRaw, CompositeDecorator
7 | } from 'draft-js';
8 | import {stateToHTML} from 'draft-js-export-html';
9 |
10 | /**
11 | * Setup hot keybinding
12 | * Cmd + S
13 | * @param e
14 | * @returns {*}
15 | */
16 | function myKeyBindingFn(e: SyntheticKeyboardEvent): string{
17 | if(e.keyCode == 83 && KeyBindingUtil.hasCommandModifier(e)){
18 | return 'save';
19 | }
20 | return getDefaultKeyBinding(e);
21 | }
22 |
23 |
24 | export default class EditorBlank extends React.Component {
25 | constructor(props) {
26 | super(props);
27 |
28 | this.state = {
29 | editorState: EditorState.createEmpty()
30 | //editorState: ContentState.createFromText('ok i i love it');
31 | //create from text
32 | };
33 |
34 | this.onChange = (editorState) => this.setState({editorState});
35 | this.focus = () => this.refs.editor.focus();
36 | this.logState = () => {
37 | console.log(this.state.editorState.getCurrentContent());
38 | }
39 | }
40 |
41 | handleKeyCommand(command){
42 | const newState= RichUtils.handleKeyCommand(this.state.editorState, command);
43 | console.log(command);
44 | if(newState){
45 | this.onChange(newState);
46 | return true;
47 | }
48 | return false;
49 | }
50 |
51 | handleKeyDown(command){
52 | console.log(command)
53 | }
54 |
55 | _onBoldClick(){
56 | this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'));
57 | }
58 |
59 | render() {
60 | const {editorState} = this.state;
61 | return (
62 |
63 |
64 | {stateToHTML(this.state.editorState.getCurrentContent())}
65 |
66 |
67 |
68 |
74 |
75 |
76 |
77 | )
78 | }
79 | }
80 |
81 | const styles = {
82 | editor: {
83 | border: '1px solid #ccc',
84 | cursor: 'text',
85 | minHeight: 80,
86 | padding: 10
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/components/form/Editors/EditorHashtag.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {
4 | Editor, EditorState, ContentState, RichUtils,
5 | getDefaultKeyBinding, KeyBindingUtil,
6 | Entity, convertToRaw, CompositeDecorator
7 | } from 'draft-js';
8 | import {stateToHTML} from 'draft-js-export-html';
9 |
10 | /**
11 | * Setup hot keybinding
12 | * Cmd + S
13 | * @param e
14 | * @returns {*}
15 | */
16 | function myKeyBindingFn(e:SyntheticKeyboardEvent):string {
17 | if (e.keyCode == 83 && KeyBindingUtil.hasCommandModifier(e)) {
18 | return 'save';
19 | }
20 | return getDefaultKeyBinding(e);
21 | }
22 |
23 | const HANDLE_REGEX = /\@[\w]+/g;
24 | const HASHTAG_REGEX = /\S*#(?:\[[^\]]+\]|\S+)/g;
25 | /*
26 | \S* # any number of non-white space characters
27 | # # matches #
28 | (?: # start non-capturing group
29 | \[ # matches [
30 | [^\]]+ # any character but ], one or more
31 | \] # matches ]
32 | | # OR
33 | \S+ # one or more non-white space characters
34 | ) # end non-capturing group
35 | */
36 | const HANDLE_LINK = /\http:\/\/(?:\[[^\]]+\]|\S+)/g;
37 |
38 |
39 | function handleStrategy(contentBlock, callback) {
40 | findWithRegex(HANDLE_REGEX, contentBlock, callback);
41 | }
42 |
43 | function hashtagStrategy(contentBlock, callback) {
44 | findWithRegex(HASHTAG_REGEX, contentBlock, callback);
45 | }
46 |
47 | function handleLink(contentBlock, callback) {
48 | findWithRegex(HANDLE_LINK, contentBlock, callback);
49 | }
50 |
51 |
52 | function findWithRegex(regex, contentBlock, callback) {
53 | const text = contentBlock.getText();
54 | let matchArr, start;
55 | while ((matchArr = regex.exec(text)) !== null) {
56 | start = matchArr.index;
57 | callback(start, start + matchArr[0].length);
58 | }
59 | }
60 |
61 | const HandleSpan = (props) => {
62 | return {props.children}
63 | }
64 |
65 | const HashtagSpan = (props) => {
66 | return {props.children}
67 | }
68 |
69 | const HandleLinkSpan = (props) => {
70 | let href = props.children[0].props.text;
71 | return {props.children}
72 | }
73 |
74 | function myBlockStyleFn(contentBlock){
75 | const type = contentBlock.getType();
76 | console.log(type);
77 | if(type === 'blockquote'){
78 | return 'superFancyBlockquote'
79 | }
80 | }
81 |
82 | export default class EditorHashtag extends React.Component {
83 | constructor(props) {
84 | super(props);
85 |
86 | const decorator = new CompositeDecorator([
87 | {
88 | strategy: hashtagStrategy,
89 | component: HashtagSpan
90 | },
91 | {
92 | strategy: handleStrategy,
93 | component: HandleSpan
94 | },{
95 | strategy: handleLink,
96 | component: HandleLinkSpan
97 | }
98 | ]);
99 |
100 | let contentState = ContentState.createFromText('http://thanhtungdp.com');
101 |
102 | this.state = {
103 | editorState: EditorState.createWithContent(contentState, decorator)
104 | };
105 | this.onChange = (editorState) => this.setState({editorState});
106 | this.focus = () => {
107 | console.log('focus');
108 | this.refs.editor.focus();
109 | }
110 | this.logState = () => {
111 | console.log(this.state.editorState.getCurrentContent());
112 | console.log(ContentState.createFromText('ok i i love it'));
113 | }
114 | }
115 |
116 | _confirmUnderline(e) {
117 | e.preventDefault();
118 | const entityKey = Entity.create('UNDERLINE', 'MUTABLE');
119 | this.setState({
120 | editorState: RichUtils.toggleLink(
121 | this.state.editorState,
122 | this.state.editorState.getSelection(),
123 | entityKey
124 | )
125 | });
126 | }
127 |
128 | render() {
129 | const {editorState} = this.state;
130 | return (
131 |
145 | )
146 | }
147 | }
148 |
149 |
150 | const styles = {
151 | handle: {
152 | color: 'blue'
153 | },
154 | hashtag: {
155 | color: 'green'
156 | },
157 | editor: {
158 | border: '1px solid #ccc',
159 | cursor: 'text',
160 | minHeight: 80,
161 | padding: 10
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/app/components/form/Editors/EditorLink.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {
4 | Editor, EditorState, ContentState, RichUtils,
5 | getDefaultKeyBinding, KeyBindingUtil,
6 | Entity, convertToRaw, CompositeDecorator
7 | } from 'draft-js';
8 | import {stateToHTML} from 'draft-js-export-html';
9 |
10 | /**
11 | * Setup hot keybinding
12 | * Cmd + S
13 | * @param e
14 | * @returns {*}
15 | */
16 | function myKeyBindingFn(e:SyntheticKeyboardEvent):string {
17 | if (e.keyCode == 83 && KeyBindingUtil.hasCommandModifier(e)) {
18 | return 'save';
19 | }
20 | return getDefaultKeyBinding(e);
21 | }
22 |
23 | const findLinkEntities = (contentBlock, callback) => {
24 | contentBlock.findEntityRanges(
25 | (character) => {
26 | const entityKey = character.getEntity();
27 | return (
28 | entityKey !== null &&
29 | Entity.get(entityKey).getType() === 'LINK'
30 | )
31 | },
32 | callback
33 | )
34 | }
35 |
36 | const Link = (props) => {
37 | const {url} = Entity.get(props.entityKey).getData();
38 | return (
39 |
40 | {props.children}
41 |
42 | )
43 | }
44 |
45 | const findUnderlineEntities = (contentBlock,callback) => {
46 | contentBlock.findEntityRanges(
47 | (character) => {
48 | const entityKey = character.getEntity();
49 | return (
50 | entityKey !== null &&
51 | Entity.get(entityKey).getType() === 'UNDERLINE'
52 | )
53 | },
54 | callback
55 | )
56 | }
57 |
58 | const Underline = (props) => {
59 | return (
60 | {props.children}
61 | )
62 | }
63 |
64 |
65 | export default class EditorLink extends React.Component {
66 | constructor(props) {
67 | super(props);
68 |
69 | const decorator = new CompositeDecorator([
70 | {
71 | strategy: findLinkEntities,
72 | component: Link
73 | },
74 | {
75 | strategy: findUnderlineEntities,
76 | component: Underline
77 | }
78 | ]);
79 |
80 | this.state = {
81 | editorState: EditorState.createEmpty(decorator)
82 | };
83 | this.onChange = (editorState) => this.setState({editorState});
84 | this.focus = () => this.refs.editor.focus();
85 | this.logState = () => {
86 | console.log(this.state.editorState.getCurrentContent());
87 | console.log(ContentState.createFromText('ok i i love it'));
88 | }
89 | }
90 |
91 | handleKeyCommand(command) {
92 | const newState = RichUtils.handleKeyCommand(this.state.editorState, command);
93 | console.log(command);
94 | if (newState) {
95 | this.onChange(newState);
96 | return true;
97 | }
98 | return false;
99 | }
100 |
101 | _onBoldClick() {
102 | this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'));
103 | }
104 |
105 | _confirmLink(e) {
106 | e.preventDefault();
107 | const {editorState} = this.state;
108 | const entityKey = Entity.create('LINK', 'SEGMENTED', {url: 'http://gamestudio.vn/public/img/logo.png'});
109 | console.log(editorState.getSelection());
110 | this.setState({
111 | editorState: RichUtils.toggleLink(
112 | editorState,
113 | editorState.getSelection(),
114 | entityKey
115 | )
116 | }, ()=> {
117 | console.log('focus');
118 | setTimeout(()=> this.refs.editor.focus(), 0);
119 | });
120 | }
121 |
122 | _confirmUnderline(e){
123 | e.preventDefault();
124 | const entityKey =Entity.create('UNDERLINE','MUTABLE');
125 | this.setState({
126 | editorState: RichUtils.toggleLink(
127 | this.state.editorState,
128 | this.state.editorState.getSelection(),
129 | entityKey
130 | )
131 | });
132 | }
133 | /*
134 | {stateToHTML(this.state.editorState.getCurrentContent())}
135 |
136 |
137 |
138 |
144 |
145 | */
146 | render() {
147 | const {editorState} = this.state;
148 | return (
149 |
150 |
151 |
152 |
153 |
154 |
155 |
165 | Heleo
166 |
167 |
168 |
169 | Hello
170 |
171 | )
172 | }
173 | //render(){
174 | // return (
175 | //
176 | // Hello
177 | //
180 | //
181 | // )
182 | //}
183 | }
184 |
185 | const styles = {
186 | editor: {
187 | border: '1px solid #ccc',
188 | cursor: 'text',
189 | minHeight: 80,
190 | padding: 10
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/app/components/form/Editors/EditorMedia.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {
4 | Editor, EditorState, ContentState, RichUtils,
5 | getDefaultKeyBinding, KeyBindingUtil,
6 | Entity, convertToRaw, CompositeDecorator,
7 | AtomicBlockUtils
8 | } from 'draft-js';
9 | import {stateToHTML} from 'draft-js-export-html';
10 |
11 | /**
12 | * Setup hot keybinding
13 | * Cmd + S
14 | * @param e
15 | * @returns {*}
16 | */
17 | function myKeyBindingFn(e:SyntheticKeyboardEvent):string {
18 | if (e.keyCode == 83 && KeyBindingUtil.hasCommandModifier(e)) {
19 | return 'save';
20 | }
21 | return getDefaultKeyBinding(e);
22 | }
23 |
24 | const HANDLE_REGEX = /\@[\w]+/g;
25 | const HASHTAG_REGEX = /\S*#(?:\[[^\]]+\]|\S+)/g;
26 | /*
27 | \S* # any number of non-white space characters
28 | # # matches #
29 | (?: # start non-capturing group
30 | \[ # matches [
31 | [^\]]+ # any character but ], one or more
32 | \] # matches ]
33 | | # OR
34 | \S+ # one or more non-white space characters
35 | ) # end non-capturing group
36 | */
37 | const HANDLE_LINK = /\http:\/\/(?:\[[^\]]+\]|\S+)/g;
38 |
39 |
40 | function handleStrategy(contentBlock, callback) {
41 | findWithRegex(HANDLE_REGEX, contentBlock, callback);
42 | }
43 |
44 | function hashtagStrategy(contentBlock, callback) {
45 | findWithRegex(HASHTAG_REGEX, contentBlock, callback);
46 | }
47 |
48 | function handleLink(contentBlock, callback) {
49 | findWithRegex(HANDLE_LINK, contentBlock, callback);
50 | }
51 |
52 |
53 | function findWithRegex(regex, contentBlock, callback) {
54 | const text = contentBlock.getText();
55 | let matchArr, start;
56 | while ((matchArr = regex.exec(text)) !== null) {
57 | start = matchArr.index;
58 | callback(start, start + matchArr[0].length);
59 | }
60 | }
61 |
62 | const HandleSpan = (props) => {
63 | return {props.children}
64 | }
65 |
66 | const HashtagSpan = (props) => {
67 | return {props.children}
68 | }
69 |
70 | const HandleLinkSpan = (props) => {
71 | let href = props.children[0].props.text;
72 | return {props.children}
73 | }
74 |
75 | function myBlockStyleFn(contentBlock){
76 | const type = contentBlock.getType();
77 | console.log(type);
78 | if(type === 'blockquote'){
79 | return 'superFancyBlockquote'
80 | }
81 | }
82 |
83 | export default class EditorHashtag extends React.Component {
84 | constructor(props) {
85 | super(props);
86 |
87 | const decorator = new CompositeDecorator([
88 | {
89 | strategy: hashtagStrategy,
90 | component: HashtagSpan
91 | },
92 | {
93 | strategy: handleStrategy,
94 | component: HandleSpan
95 | },{
96 | strategy: handleLink,
97 | component: HandleLinkSpan
98 | }
99 | ]);
100 |
101 | let contentState = ContentState.createFromText('http://thanhtungdp.com');
102 |
103 | this.state = {
104 | editorState: EditorState.createWithContent(contentState, decorator)
105 | };
106 | this.onChange = (editorState) => this.setState({editorState});
107 | this.focus = () => {
108 | console.log('focus');
109 | this.refs.editor.focus();
110 | }
111 | this.logState = () => {
112 | console.log(this.state.editorState.getCurrentContent());
113 | console.log(ContentState.createFromText('ok i i love it'));
114 | }
115 | }
116 |
117 | _addMedia(type){
118 | const src = 'https://dl4rygnmrir77.cloudfront.net/muzli_feed/wp-content/uploads/2016/05/02001335/Screenshot-at-May-02-00-12-42.png'
119 | const entityKey = Entity.create(type, 'IMMUTABLE',{src});
120 |
121 | return AtomicBlockUtils.insertAtomicBlock(
122 | this.state.editorState,
123 | entityKey,
124 | ' '
125 | )
126 | }
127 |
128 | _addImage(){
129 | this.onChange(this._addMedia('image'));
130 | }
131 |
132 | _addYoutube(){
133 | this.onChange(this._addMedia('video_youtube'));
134 | }
135 |
136 | _confirmUnderline(e) {
137 | e.preventDefault();
138 | const entityKey = Entity.create('UNDERLINE', 'MUTABLE');
139 | this.setState({
140 | editorState: RichUtils.toggleLink(
141 | this.state.editorState,
142 | this.state.editorState.getSelection(),
143 | entityKey
144 | )
145 | });
146 | }
147 |
148 | render() {
149 | const {editorState} = this.state;
150 | return (
151 |
152 |
153 |
154 |
155 |
166 |
167 |
168 | )
169 | }
170 | }
171 |
172 | function mediaBlockRenderer(block){
173 | if(block.getType() === 'atomic'){
174 | return {
175 | component: Media,
176 | editable: false
177 | }
178 | }
179 | return null;
180 | }
181 |
182 | const Image = (props) => {
183 | return
184 | }
185 |
186 | const VideoYoutube = (props)=>{
187 | return
188 | }
189 |
190 | const Media = (props) => {
191 | const entity = Entity.get(props.block.getEntityAt(0));
192 | const {src} = entity.getData();
193 | const type = entity.getType();
194 |
195 | let media;
196 | if(type === 'image'){
197 | media =
198 | }
199 | if(type === 'video_youtube'){
200 | media =
201 | }
202 | return media;
203 | }
204 |
205 | const styles = {
206 | handle: {
207 | color: 'blue'
208 | },
209 | hashtag: {
210 | color: 'green'
211 | },
212 | editor: {
213 | border: '1px solid #ccc',
214 | cursor: 'text',
215 | minHeight: 80,
216 | padding: 10
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/app/components/form/Editors/EditorRich.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {
4 | Editor, EditorState, ContentState, RichUtils,
5 | getDefaultKeyBinding, KeyBindingUtil,
6 | Entity, convertToRaw, CompositeDecorator,
7 | convertFromRaw,
8 | AtomicBlockUtils, Modifier
9 | } from 'draft-js';
10 | import {stateToHTML} from 'draft-js-export-html';
11 |
12 | const HANDLE_LINK = /\http:\/\/(?:\[[^\]]+\]|\S+)/g;
13 |
14 | function handleLink(contentBlock, callback) {
15 | findWithRegex(HANDLE_LINK, contentBlock, callback);
16 | }
17 |
18 | const HandleLinkSpan = (props) => {
19 | let href = props.children[0].props.text;
20 | return {props.children}
21 | }
22 |
23 |
24 | function findWithRegex(regex, contentBlock, callback) {
25 | const text = contentBlock.getText();
26 | let matchArr, start;
27 | while ((matchArr = regex.exec(text)) !== null) {
28 | start = matchArr.index;
29 | callback(start, start + matchArr[0].length);
30 | }
31 | }
32 |
33 | export default class EditorRich extends React.Component {
34 | constructor(props) {
35 | super(props);
36 |
37 | this.decorator = new CompositeDecorator([
38 | {
39 | strategy: handleLink,
40 | component: HandleLinkSpan
41 | }
42 | ]);
43 |
44 | this.state = {
45 | editorState: EditorState.createEmpty(this.decorator)
46 | };
47 | this.onChange = (editorState) => {
48 | this.setState({editorState});
49 | let contentState = editorState.getCurrentContent();
50 |
51 | if (contentState.getPlainText()) {
52 | this.props.onChange(convertToRaw(contentState));
53 | }
54 | else {
55 | this.props.onChange(null);
56 | }
57 | }
58 | this.focus = () => {
59 | //this.refs.editor.focus();
60 | }
61 | }
62 |
63 | _toggleBlockType(blockType) {
64 | this.onChange(
65 | RichUtils.toggleBlockType(
66 | this.state.editorState,
67 | blockType
68 | )
69 | )
70 | }
71 |
72 | _toggleInlineStyle(inlineStyle) {
73 | this.onChange(
74 | RichUtils.toggleInlineStyle(
75 | this.state.editorState,
76 | inlineStyle
77 | )
78 | )
79 | }
80 |
81 | handleKeyCommand(command) {
82 | const newState = RichUtils.toggleBlockType(this.state.editorState, command);
83 | this.onChange(newState);
84 | }
85 |
86 | handleTab(e) {
87 | e.preventDefault();
88 | let contentState = this.state.editorState.getCurrentContent();
89 | let targetRange = this.state.editorState.getSelection();
90 | let newContentState = Modifier.insertText(
91 | contentState,
92 | targetRange,
93 | '\t'
94 | );
95 | let editorState = EditorState.push(
96 | this.state.editorState,
97 | newContentState
98 | );
99 |
100 | this.onChange(editorState)
101 | this.focus();
102 | }
103 |
104 | componentDidMount() {
105 | if (this.props.defaultContentState) {
106 | let newRawContent = {...this.props.defaultContentState, entityMap: {}}
107 | let newContentState = convertFromRaw(newRawContent);
108 | this.onChange(EditorState.createWithContent(newContentState, this.decorator));
109 | }
110 | }
111 |
112 | render() {
113 | const {editorState} = this.state;
114 |
115 | let className = !this.props.readOnly ? 'RichEditor-editor' : null;
116 | var contentState = editorState.getCurrentContent();
117 | if (!contentState.hasText()) {
118 | if (contentState.getBlockMap().first().getType() !== 'unstyled') {
119 | className += ' RichEditor-hidePlaceholder';
120 | }
121 | }
122 |
123 | return (
124 |
125 | {!this.props.readOnly &&
126 |
127 |
128 |
129 |
130 | }
131 |
132 |
145 |
146 |
147 | )
148 | }
149 | }
150 |
151 | function myKeyBindingFn(e:SyntheticKeyboardEvent):string {
152 | if (e.keyCode == 69 && KeyBindingUtil.hasCommandModifier(e)) {
153 | return 'code-block';
154 | }
155 | return getDefaultKeyBinding(e);
156 | }
157 |
158 | const styleMap = {
159 | CODE: {
160 | backgroundColor: 'rgba(0, 0, 0, 0.05)',
161 | fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
162 | fontSize: 16,
163 | padding: 2,
164 | },
165 | };
166 |
167 | class StyleButton extends React.Component {
168 | constructor() {
169 | super(...arguments);
170 | this.onToggle = (e) => {
171 | e.preventDefault();
172 | this.props.onToggle(this.props.style)
173 | }
174 | }
175 |
176 | render() {
177 | let className = 'RichEditor-styleButton';
178 | if (this.props.active) {
179 | className += ' RichEditor-activeButton';
180 | }
181 | return (
182 |
183 | {this.props.label}
184 |
185 | )
186 | }
187 | }
188 |
189 | const BLOCK_TYPES = [
190 | {label: 'H1', style: 'header-one'},
191 | {label: 'H2', style: 'header-two'},
192 | {label: 'H3', style: 'header-three'},
193 | {label: 'H4', style: 'header-four'},
194 | {label: 'H5', style: 'header-five'},
195 | {label: 'H6', style: 'header-six'},
196 | {label: 'Blockquote', style: 'blockquote'},
197 | {label: 'UL', style: 'unordered-list-item'},
198 | {label: 'OL', style: 'ordered-list-item'},
199 | {label: 'Code Block', style: 'code-block'},
200 | ];
201 |
202 | const BlockStyleControls = (props) => {
203 | const {editorState} = props;
204 | const selection = editorState.getSelection();
205 | const blockType = editorState.getCurrentContent()
206 | .getBlockForKey(selection.getStartKey())
207 | .getType();
208 | return (
209 |
210 | {BLOCK_TYPES.map(type =>
211 |
218 | )}
219 |
220 | )
221 | }
222 |
223 | var INLINE_STYLES = [
224 | {label: 'Bold', style: 'BOLD'},
225 | {label: 'Italic', style: 'ITALIC'},
226 | {label: 'Underline', style: 'UNDERLINE'},
227 | {label: 'Monospace', style: 'CODE'},
228 | ];
229 |
230 | const InlineStyleControls = (props) => {
231 | const {editorState} = props;
232 | const curentStyle = editorState.getCurrentInlineStyle();
233 | return (
234 |
235 | {INLINE_STYLES.map(type =>
236 |
243 | )}
244 |
245 | )
246 | }
247 |
248 | EditorRich.defaultProps = {
249 | onChange: () => {
250 |
251 | }
252 | }
--------------------------------------------------------------------------------
/app/components/form/Editors/EditorRichWithTab.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {
4 | Editor, EditorState, ContentState, RichUtils,
5 | getDefaultKeyBinding, KeyBindingUtil,
6 | Entity, convertToRaw, CompositeDecorator,
7 | AtomicBlockUtils, Modifier
8 | } from 'draft-js';
9 | import {stateToHTML} from 'draft-js-export-html';
10 |
11 | /**
12 | * Setup hot keybinding
13 | * Cmd + S
14 | * @param e
15 | * @returns {*}
16 | */
17 | function myKeyBindingFn(e:SyntheticKeyboardEvent):string {
18 | if (e.keyCode == 83 && KeyBindingUtil.hasCommandModifier(e)) {
19 | return 'save';
20 | }
21 | return getDefaultKeyBinding(e);
22 | }
23 |
24 | const HANDLE_REGEX = /\@[\w]+/g;
25 | const HASHTAG_REGEX = /\S*#(?:\[[^\]]+\]|\S+)/g;
26 | /*
27 | \S* # any number of non-white space characters
28 | # # matches #
29 | (?: # start non-capturing group
30 | \[ # matches [
31 | [^\]]+ # any character but ], one or more
32 | \] # matches ]
33 | | # OR
34 | \S+ # one or more non-white space characters
35 | ) # end non-capturing group
36 | */
37 | const HANDLE_LINK = /\http:\/\/(?:\[[^\]]+\]|\S+)/g;
38 |
39 |
40 | function handleStrategy(contentBlock, callback) {
41 | findWithRegex(HANDLE_REGEX, contentBlock, callback);
42 | }
43 |
44 | function hashtagStrategy(contentBlock, callback) {
45 | findWithRegex(HASHTAG_REGEX, contentBlock, callback);
46 | }
47 |
48 | function handleLink(contentBlock, callback) {
49 | findWithRegex(HANDLE_LINK, contentBlock, callback);
50 | }
51 |
52 |
53 | function findWithRegex(regex, contentBlock, callback) {
54 | const text = contentBlock.getText();
55 | let matchArr, start;
56 | while ((matchArr = regex.exec(text)) !== null) {
57 | start = matchArr.index;
58 | callback(start, start + matchArr[0].length);
59 | }
60 | }
61 |
62 | const HandleSpan = (props) => {
63 | return {props.children}
64 | }
65 |
66 | const HashtagSpan = (props) => {
67 | return {props.children}
68 | }
69 |
70 | const HandleLinkSpan = (props) => {
71 | let href = props.children[0].props.text;
72 | return {props.children}
73 | }
74 |
75 | function myBlockStyleFn(contentBlock) {
76 | const type = contentBlock.getType();
77 | console.log(type);
78 | if (type === 'blockquote') {
79 | return 'superFancyBlockquote'
80 | }
81 | }
82 |
83 | export default class EditorRich extends React.Component {
84 | constructor(props) {
85 | super(props);
86 |
87 | const decorator = new CompositeDecorator([
88 | {
89 | strategy: hashtagStrategy,
90 | component: HashtagSpan
91 | },
92 | {
93 | strategy: handleStrategy,
94 | component: HandleSpan
95 | }, {
96 | strategy: handleLink,
97 | component: HandleLinkSpan
98 | }
99 | ]);
100 |
101 | let contentState = ContentState.createFromText('http://thanhtungdp.com');
102 |
103 | this.state = {
104 | editorState: EditorState.createWithContent(contentState, decorator)
105 | };
106 | this.onChange = (editorState) => this.setState({editorState});
107 | this.focus = () => {
108 | console.log('focus');
109 | this.refs.editor.focus();
110 | }
111 | this.logState = () => {
112 | console.log(this.state.editorState.getCurrentContent());
113 | console.log(ContentState.createFromText('ok i i love it'));
114 | }
115 | }
116 |
117 | _addMedia(type) {
118 | const src = 'https://dl4rygnmrir77.cloudfront.net/muzli_feed/wp-content/uploads/2016/05/02001335/Screenshot-at-May-02-00-12-42.png'
119 | const entityKey = Entity.create(type, 'IMMUTABLE', {src});
120 |
121 | return AtomicBlockUtils.insertAtomicBlock(
122 | this.state.editorState,
123 | entityKey,
124 | ' '
125 | )
126 | }
127 |
128 | _toggleBlockType(blockType){
129 | this.onChange(
130 | RichUtils.toggleBlockType(
131 | this.state.editorState,
132 | blockType
133 | )
134 | )
135 | }
136 |
137 | _toggleInlineStyle(inlineStyle){
138 | this.onChange(
139 | RichUtils.toggleInlineStyle(
140 | this.state.editorState,
141 | inlineStyle
142 | )
143 | )
144 | }
145 |
146 | handleKeyCommand(command){
147 | const newState = RichUtils.toggleBlockType(this.state.editorState, command);
148 | console.log(command);
149 | this.onChange(newState);
150 | }
151 |
152 | handleTab(e){
153 | console.log(e);
154 | e.preventDefault();
155 | let contentState = this.state.editorState.getCurrentContent();
156 | let targetRange = this.state.editorState.getSelection();
157 | let newContentState = Modifier.insertText(
158 | contentState,
159 | targetRange,
160 | '\t'
161 | );
162 | let editorState = EditorState.push(
163 | this.state.editorState,
164 | newContentState
165 | );
166 |
167 | this.onChange(editorState)
168 | this.focus();
169 | }
170 |
171 | render() {
172 | const {editorState} = this.state;
173 |
174 | let className='RichEditor-editor';
175 | var contentState = editorState.getCurrentContent();
176 | if(!contentState.hasText()){
177 | if(contentState.getBlockMap().first().getType() !== 'unstyled'){
178 | className += ' RichEditor-hidePlaceholder';
179 | }
180 | }
181 | return (
182 |
183 |
184 |
185 |
186 |
200 |
201 |
202 | )
203 | }
204 | }
205 |
206 | function myKeyBindingFn(e: SyntheticKeyboardEvent): string{
207 | if(e.keyCode ==69 && KeyBindingUtil.hasCommandModifier(e)){
208 | return 'code-block';
209 | }
210 | return getDefaultKeyBinding(e);
211 | }
212 |
213 | const styleMap = {
214 | CODE: {
215 | backgroundColor: 'rgba(0, 0, 0, 0.05)',
216 | fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
217 | fontSize: 16,
218 | padding: 2,
219 | },
220 | };
221 |
222 | function getBlockStyle(block) {
223 | switch (block.getType()) {
224 | case 'blockquote':
225 | return 'Richeditor-blockquote';
226 | default:
227 | return null;
228 | }
229 | }
230 |
231 | class StyleButton extends React.Component {
232 | constructor() {
233 | super(...arguments);
234 | this.onToggle = (e) => {
235 | e.preventDefault();
236 | this.props.onToggle(this.props.style)
237 | }
238 | }
239 |
240 | render() {
241 | let className = 'RichEditor-styleButton';
242 | if (this.props.active) {
243 | className += ' RichEditor-activeButton';
244 | }
245 | return (
246 |
247 | {this.props.label}
248 |
249 | )
250 | }
251 | }
252 |
253 | const BLOCK_TYPES = [
254 | {label: 'H1', style: 'header-one'},
255 | {label: 'H2', style: 'header-two'},
256 | {label: 'blockquote', style: 'blockquote'},
257 | {label: 'Code block', style: 'code-block'}
258 | ]
259 |
260 | const BlockStyleControls = (props) => {
261 | const {editorState} = props;
262 | const selection = editorState.getSelection();
263 | const blockType = editorState.getCurrentContent()
264 | .getBlockForKey(selection.getStartKey())
265 | .getType();
266 | return (
267 |
268 | {BLOCK_TYPES.map(type =>
269 |
276 | )}
277 |
278 | )
279 | }
280 |
281 | var INLINE_STYLES = [
282 | {label: 'Bold', style: 'BOLD'},
283 | {label: 'Italic', style: 'ITALIC'},
284 | {label: 'Underline', style: 'UNDERLINE'},
285 | {label: 'Monospace', style: 'CODE'},
286 | ];
287 |
288 | const InlineStyleControls = (props) => {
289 | const {editorState} = props;
290 | const curentStyle = editorState.getCurrentInlineStyle();
291 | return (
292 |
293 | {INLINE_STYLES.map(type =>
294 |
301 | )}
302 |
303 | )
304 | }
305 |
306 | const styles = {
307 | handle: {
308 | color: 'blue'
309 | },
310 | hashtag: {
311 | color: 'green'
312 | },
313 | editor: {
314 | border: '1px solid #ccc',
315 | cursor: 'text',
316 | minHeight: 80,
317 | padding: 10
318 | }
319 | }
320 |
--------------------------------------------------------------------------------
/app/components/form/Editors/index.js:
--------------------------------------------------------------------------------
1 | import EditorBlank from './EditorBlank';
2 | import EditorLink from './EditorLink';
3 | import EditorHashtag from './EditorHashtag';
4 | import EditorMedia from './EditorMedia';
5 | import EditorRich from './EditorRich';
6 |
7 | export default {
8 | EditorBlank,
9 | EditorLink,
10 | EditorHashtag,
11 | EditorMedia,
12 | EditorRich
13 | }
14 |
15 | export {EditorBlank, EditorLink, EditorHashtag, EditorMedia, EditorRich};
--------------------------------------------------------------------------------
/app/components/form/InputText.js:
--------------------------------------------------------------------------------
1 | import React,{Component, PropTypes} from 'react';
2 | import {FormGroup, ControlLabel, FormControl} from 'react-bootstrap';
3 | import ValidateWrapControl from './ValidateWrapControl';
4 | import validator from 'validator';
5 |
6 | export default class InputText extends Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/app/components/form/Loading.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 |
3 | export default class Loading extends Component {
4 | render() {
5 | return (
6 |
7 |
8 |
9 |
14 | {this.props.text &&
15 |
16 | {this.props.text} ...
17 |
18 | }
19 |
20 |
21 | )
22 | }
23 | }
24 |
25 | Loading.propTypes = {
26 | text: PropTypes.string
27 | }
--------------------------------------------------------------------------------
/app/components/form/Textarea.js:
--------------------------------------------------------------------------------
1 | import React,{Component, PropTypes} from 'react';
2 | import ValidateWrapControl from './ValidateWrapControl';
3 | import {FormControl} from 'react-bootstrap';
4 |
5 | export default class Textarea extends Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render() {
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/app/components/form/ValidateWrapControl.js:
--------------------------------------------------------------------------------
1 | import React,{Component, PropTypes} from 'react';
2 | import {FormGroup, ControlLabel, FormControl} from 'react-bootstrap';
3 |
4 | export default class ValidateWrapControl extends Component {
5 | constructor() {
6 | super(...arguments)
7 | }
8 |
9 | render() {
10 | const {title, touched, error} = this.props;
11 | const status = (touched && error) ? 'error' : null;
12 |
13 | return (
14 |
15 | {title && {title}}
16 | {this.props.children}
17 | {touched && error && {error}}
18 |
19 | )
20 | }
21 | }
22 |
23 | ValidateWrapControl.propTypes = {
24 | title: PropTypes.string,
25 | touched: PropTypes.bool,
26 | error: PropTypes.string
27 | }
--------------------------------------------------------------------------------
/app/components/form/WrapContainer.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Grid, Col} from 'react-bootstrap';
3 | import classNames from 'classnames';
4 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
5 | import WrapTransitions from './WrapTransitions';
6 |
7 | export default class WrapContainer extends Component {
8 | getWrapClassName() {
9 | let className = [
10 | 'bg-white'
11 | ]
12 | className.push('animated');
13 | if(this.props.out){
14 | className.push(this.props.animateOut);
15 | }else {
16 | if (this.props.animateIn) {
17 | className.push(this.props.animateIn);
18 | }
19 | }
20 | return classNames(className);
21 | }
22 |
23 | render() {
24 | return (
25 |
26 |
27 | {this.props.children}
28 |
29 |
30 | )
31 | }
32 | }
33 |
34 | WrapContainer.propTypes = {}
--------------------------------------------------------------------------------
/app/components/form/WrapTransitions.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | export default class WrapTransitions extends Component {
5 | componentWillAppear(callback) {
6 | this._animateIn(callback);
7 | }
8 |
9 | _animateIn(callback) {
10 | let el = ReactDOM.findDOMNode(this);
11 | el.classList.remove('animated')
12 | el.classList.add('fadeIn')
13 | el.classList.add('animated');
14 | setTimeout(()=> {
15 | callback();
16 | }, 2000);
17 | }
18 |
19 | _animateOut(callback) {
20 | let el = ReactDOM.findDOMNode(this);
21 | el.classList.remove('animated')
22 | el.classList.add('fadeOut');
23 | el.classList.add('animated');
24 | setTimeout(()=> {
25 | callback();
26 | }, 2000);
27 | }
28 |
29 | componentWillEnter(callback) {
30 | console.log('enter');
31 | this._animateIn(callback);
32 | }
33 |
34 | componentWillLeave(callback) {
35 | console.log('leave');
36 | this._animateOut(callback);
37 | }
38 |
39 | render() {
40 | return (
41 |
42 | {this.props.children}
43 |
44 | )
45 | }
46 | }
47 |
48 | WrapTransitions.propTypes = {}
--------------------------------------------------------------------------------
/app/components/form/index.js:
--------------------------------------------------------------------------------
1 | import InputText from './InputText';
2 | import Textarea from './Textarea';
3 | import Editors from './Editors/index';
4 | import ValidateWrapControl from './ValidateWrapControl';
5 | import WrapContainer from './WrapContainer';
6 | import Loading from './Loading';
7 | import WrapTransitions from './WrapTransitions';
8 |
9 | export default {
10 | InputText,
11 | Textarea,
12 | Editors,
13 | ValidateWrapControl,
14 | WrapContainer,
15 | Loading
16 | }
17 |
18 | export {InputText, Textarea, Editors, ValidateWrapControl, WrapContainer, Loading, WrapTransitions}
--------------------------------------------------------------------------------
/app/components/layouts/master/AppMaster.js:
--------------------------------------------------------------------------------
1 | //Import React
2 | import React,{Component} from 'react';
3 | import {Link} from 'react-router';
4 | import {connect} from 'react-redux';
5 | import {bindActionCreators} from 'redux';
6 | import ReactTransitionGroup from 'react-addons-transition-group'
7 |
8 | // Action
9 | import {checkToken, getProfile} from '../../../redux/actions/AuthAction';
10 |
11 | // Components
12 | import Header from './partials/Header';
13 | import {WrapTransitions} from '../../../components/form/index';
14 |
15 | export default class AppMaster extends Component {
16 |
17 | constructor() {
18 | super(...arguments);
19 | }
20 |
21 | componentDidUpdate() {
22 | if (!this.props.auth.guest && !this.props.auth.profile.updated_at) {
23 | this.props.getProfile();
24 | }
25 | }
26 |
27 | componentDidMount() {
28 | this.props.checkToken();
29 | this.setState({firstRender: true})
30 | }
31 |
32 | render() {
33 | return (
34 |
35 |
36 | {this.props.children}
37 |
38 | )
39 | }
40 | }
41 |
42 | const mapStateToProps = (state) => {
43 | return {
44 | auth: state.auth.authenticated
45 | }
46 | }
47 |
48 | const mapDispatchToProps = (dipsatch) => {
49 | return bindActionCreators({getProfile, checkToken}, dipsatch);
50 | }
51 |
52 | export default connect(mapStateToProps, mapDispatchToProps)(AppMaster);
--------------------------------------------------------------------------------
/app/components/layouts/master/partials/Header.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react';
2 | import {Navbar, Nav, Col, Grid, NavItem, NavDropdown, MenuItem} from 'react-bootstrap';
3 | import Logo from './Logo';
4 |
5 | export default class Header extends Component {
6 | render() {
7 | const {auth: {guest, user, profile}} = this.props;
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
26 | { guest ?
27 | :
31 | profile.updated_at?
32 |
36 | :null
37 | }
38 |
39 |
40 |
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/components/layouts/master/partials/Logo.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 |
3 | export default class Logo extends Component {
4 | componentDidMount(){
5 | setInterval(()=>{
6 | let underline = document.getElementById('logo-underline');
7 | if(!underline.classList.contains('hide')){
8 | underline.classList.add('hide');
9 | }
10 | else{
11 | underline.classList.remove('hide');
12 | }
13 | }, 800)
14 | }
15 | render() {
16 | return (
17 |
18 |

19 |
_
20 |
21 | )
22 | }
23 | }
24 |
25 | Logo.propTypes = {
26 |
27 | }
--------------------------------------------------------------------------------
/app/components/pages/member/Login.js:
--------------------------------------------------------------------------------
1 | import React,{PropTypes} from 'react';
2 | import {Form, Button, Grid, Col} from 'react-bootstrap';
3 | import {InputText, WrapContainer, Loading} from '../../form/index';
4 |
5 | export default class Login extends React.Component {
6 | onSubmit() {
7 | this.props.onSubmit(this.props.fields.email.value, this.props.fields.password.value);
8 | }
9 |
10 | render() {
11 | let {fields:{email, password},awaitStatuses, awaitErrors, handleSubmit} = this.props;
12 | return (
13 |
14 |
15 | Login
16 |
24 |
25 |
26 | )
27 | }
28 | }
29 |
30 | Login.propTypes = {
31 | fields: PropTypes.object.isRequired,
32 | awaitStatuses: PropTypes.shape({
33 | userLogin: PropTypes.string
34 | }),
35 | awaitErrors: PropTypes.shape({
36 | userLogin: PropTypes.string
37 | }),
38 | handleSubmit: PropTypes.func.isRequired,
39 | onSubmit: PropTypes.func.isRequired,
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/pages/member/Profile.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Grid,Col,Button} from 'react-bootstrap';
3 | import {InputText, Textarea, WrapContainer, Loading} from '../../form/index';
4 |
5 | export default class Profile extends Component {
6 |
7 | constructor() {
8 | super(...arguments);
9 | }
10 |
11 | onSumbit() {
12 | let profile = {
13 | first_name: this.props.fields.first_name.value,
14 | last_name: this.props.fields.last_name.value,
15 | description: this.props.fields.description.value
16 | }
17 | this.props.onSubmit(profile);
18 | }
19 |
20 | render() {
21 | const {fields:{first_name, last_name, description}, handleSubmit, awaitStatuses} = this.props;
22 | return (
23 |
24 | Profile
25 | {awaitStatuses.getProfile == 'pending' && }
26 | {awaitStatuses.getProfile == 'success' &&
27 |
33 | }
34 | {awaitStatuses.updateProfile == 'pending' && }
35 |
36 | )
37 | }
38 | }
39 |
40 | Profile.propTypes = {
41 | fields: PropTypes.object.isRequired,
42 | awaitStatuses: PropTypes.shape({
43 | getProfile: PropTypes.string,
44 | updateProfile: PropTypes.string
45 | }),
46 | handleSubmit: PropTypes.func.isRequired,
47 | onSubmit: PropTypes.func.isRequired
48 | }
--------------------------------------------------------------------------------
/app/components/pages/member/Register.js:
--------------------------------------------------------------------------------
1 | import React,{PropTypes} from 'react';
2 | import {Form, Button, Grid, Col} from 'react-bootstrap';
3 | import {bindActionCreators} from 'redux';
4 | import {authRegister} from '../../../redux/actions/AuthAction';
5 | import {InputText, WrapContainer, Loading} from '../../form/index';
6 |
7 | export default class Register extends React.Component {
8 | onSubmit() {
9 | const {email, password, first_name, last_name} = this.props.fields;
10 | this.props.onSubmit(email.value, password.value, {
11 | first_name: first_name.value,
12 | last_name: last_name.value
13 | });
14 | }
15 |
16 | render() {
17 | const {awaitStatuses, awaitErrors, handleSubmit, submitting}= this.props;
18 | const {email, first_name, last_name, password, re_password} = this.props.fields;
19 | return (
20 |
21 | Register
22 |
34 |
35 | )
36 | }
37 | }
38 |
39 | Register.propTypes = {
40 | fields: PropTypes.object.isRequired,
41 | awaitStatuses: PropTypes.shape({
42 | userLogin: PropTypes.string,
43 | userRegister: PropTypes.string
44 | }).isRequired,
45 | awaitErrors: PropTypes.shape({
46 | userRegister: PropTypes.string
47 | }),
48 | handleSubmit: PropTypes.func.isRequired,
49 | onSubmit: PropTypes.func.isRequired,
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/app/components/pages/posts/PostForm.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Grid, Col, Button} from 'react-bootstrap';
3 | import {InputText, Textarea, Editors, Loading, ValidateWrapControl, WrapContainer} from '../../form/index';
4 | import {Link} from 'react-router';
5 |
6 | export default class PostForm extends Component {
7 | onSubmit() {
8 | let post = {
9 | title: this.props.fields.title.value,
10 | description: this.props.fields.description.value,
11 | content: this.props.fields.content.value
12 | }
13 |
14 | this.props.onSubmit(post);
15 | }
16 |
17 | render() {
18 | const {fields: {title, description, content}, awaitStatuses, formType, handleSubmit, submitting} = this.props;
19 | return (
20 |
21 |
22 |
23 | {this.props.formType == 'edit' ? 'Edit Post' : 'New Post'}
24 | {' '}
25 |
26 |
27 | {this.props.formType == 'edit' &&
28 |
29 | View {' '}
30 |
33 |
34 | }
35 |
36 |
37 |
38 |
67 |
68 | )
69 | }
70 | }
71 |
72 | PostForm.propTypes = {
73 | formType: PropTypes.oneOf(['create', 'edit']),
74 | post_id: PropTypes.string,
75 | fields: PropTypes.shape({
76 | title: PropTypes.object,
77 | description: PropTypes.object,
78 | content: PropTypes.object
79 | }),
80 | awaitStatuses: PropTypes.shape({
81 | createPost: PropTypes.string,
82 | updatePost: PropTypes.string,
83 | deletePost: PropTypes.string
84 | }),
85 | awaitErrors: PropTypes.shape({
86 | createPost: PropTypes.string,
87 | updatePost: PropTypes.string,
88 | deletePost: PropTypes.string
89 | }),
90 | onSubmit: PropTypes.func,
91 | onDelete: PropTypes.func,
92 | handleSubmit: PropTypes.func,
93 | submitting: PropTypes.bool
94 | }
--------------------------------------------------------------------------------
/app/components/pages/posts/PostView.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Grid, Col} from 'react-bootstrap';
3 | import {Editors, WrapContainer, Loading} from '../../form/index';
4 |
5 | export default class PostView extends Component {
6 | constructor() {
7 | super(...arguments)
8 | }
9 |
10 | render() {
11 | let {post, isAuthor, awaitStatuses, awaitErrors} = this.props;
12 | return (
13 |
14 | {awaitStatuses.getPost == 'pending' && }
15 | {awaitStatuses.getPost == 'success' &&
16 |
17 |
18 |
19 |
{post.title}
20 | {post.user &&
21 |
22 |
23 | - {post.user.first_name}
24 |
25 |
26 | }
27 |
28 | {isAuthor &&
29 |
Edit
30 | }
31 |
32 |
33 |
34 |
35 |
36 | }
37 | {awaitErrors.getPost &&
38 | {awaitErrors.getPost}
39 | }
40 |
41 | )
42 | }
43 | }
44 |
45 | PostView.propTypes = {
46 | post: PropTypes.shape({
47 | user: PropTypes.object
48 | }),
49 | isAuthor: PropTypes.bool,
50 | awaitStatuses:PropTypes.shape({
51 | getPost: PropTypes.string
52 | }),
53 | awaitErrors:PropTypes.shape({
54 | getPost: PropTypes.string
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/app/components/pages/posts/PostsList.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Grid, Row, Col} from 'react-bootstrap';
3 | import Masonry from 'react-masonry-component';
4 | import PostItem from './partials/PostItem';
5 | import {Loading, WrapContainer} from '../../form/index';
6 | import {postsList} from '../../../configs/index';
7 |
8 | export default class PostsList extends Component {
9 | constructor() {
10 | super(...arguments);
11 | this.state = {
12 | redirecting: false
13 | }
14 | }
15 |
16 | onClick(e) {
17 | e.preventDefault();
18 | let url = e.target.href;
19 | this.setState({redirecting: true});
20 | setTimeout(()=> {
21 | window.location = url;
22 | }, 300);
23 | }
24 |
25 | componentDidMount() {
26 | window.addEventListener('scroll', function () {
27 | if (this.props.awaitStatuses.getPosts == 'success') {
28 | console.log(document.body.scrollHeight + 'vs' + document.body.scrollTop + 'vs' + window.innerHeight);
29 | if (document.body.scrollHeight <= document.body.scrollTop + window.innerHeight) {
30 | this.props.loadMore();
31 | }
32 | }
33 | }.bind(this));
34 | }
35 |
36 | render() {
37 | let posts = this.props.posts.slice(0, this.props.currentItems).map(post =>
38 |
39 |
40 |
41 | );
42 |
43 |
44 | return (
45 |
46 | Posts List
47 |
48 |
49 |
50 | {posts}
51 |
52 | { this.props.currentItems < this.props.posts.length &&
53 |
54 |
55 |
56 | }
57 |
58 |
59 | {this.props.awaitStatuses.getPosts == 'pending' && }
60 |
61 | )
62 | }
63 | }
64 |
65 | PostsList.propTypes = {
66 | posts: PropTypes.array.isRequired,
67 | awaitStatuses: PropTypes.shape({
68 | getPosts: PropTypes.string
69 | })
70 | }
--------------------------------------------------------------------------------
/app/components/pages/posts/index.js:
--------------------------------------------------------------------------------
1 | import PostForm from './PostForm';
2 | import PostsList from './PostsList';
3 | import PostView from './PostView';
4 |
5 | export default {PostForm, PostsList, PostView};
6 |
7 | export {PostForm, PostsList, PostView};
--------------------------------------------------------------------------------
/app/components/pages/posts/partials/PostItem.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {Col} from 'react-bootstrap';
3 |
4 | export default class PostItem extends Component {
5 | render() {
6 | let post = this.props.post;
7 | return (
8 |
9 |
12 |
13 | {post.description}
14 |
15 |
{post.user.first_name}
16 |
17 | )
18 | }
19 | }
20 |
21 | PostItem.propTypes = {
22 | post: PropTypes.object
23 | }
--------------------------------------------------------------------------------
/app/configs/index.js:
--------------------------------------------------------------------------------
1 | const postsList = {
2 | perPage: 5
3 | }
4 |
5 | export {
6 | postsList
7 | }
--------------------------------------------------------------------------------
/app/redux/Root.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Provider} from 'react-redux';
3 | import configureStore from './store/configureStore';
4 | import {Router, Route, hashHistory} from 'react-router';
5 | import { syncHistoryWithStore, routerReducer } from 'react-router-redux';
6 | import Routes from '../routes/index';
7 |
8 | const store = configureStore();
9 |
10 | export default class Root extends Component {
11 | render() {
12 | return (
13 |
14 | window.scrollTo(0, 0)} history={hashHistory}>
15 | {Routes()}
16 |
17 |
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/redux/actions/AuthAction.js:
--------------------------------------------------------------------------------
1 | import {AWAIT_MARKER} from 'redux-await';
2 | import Auth from '../../api/auth/index';
3 |
4 | export const AUTH_LOGIN = 'auth login';
5 |
6 | export const AUTH_REGISTER = 'auth register';
7 |
8 | export const AUTH_GET_PROFILE = 'auth get profile'
9 |
10 | export const AUTH_UPDATE_PROFILE = 'auth update profile fetching';
11 |
12 | export const AUTH_CHECK_TOKEN = 'auth check token';
13 |
14 | export const AUTH_LOGOUT = 'auth logout'
15 |
16 | /**
17 | * Login with email, password
18 | * @param email
19 | * @param password
20 | * @returns {Function}
21 | */
22 | export function authLogin(email, password) {
23 | return (dispatch)=> {
24 | dispatch({
25 | type: AUTH_LOGIN,
26 | AWAIT_MARKER,
27 | payload: {
28 | userLogin: Auth.login(email, password)
29 | }
30 | });
31 | }
32 | }
33 |
34 | /**
35 | * Register and login
36 | * @param email
37 | * @param password
38 | * @returns {Function}
39 | */
40 | export function authRegister(email, password, profile = {}) {
41 | return (dispatch)=> {
42 | dispatch({
43 | type: AUTH_REGISTER,
44 | AWAIT_MARKER,
45 | payload: {
46 | userRegister: Auth.register(email, password, profile)
47 | }
48 | });
49 | }
50 | }
51 |
52 | export function updateProfile(profile) {
53 | return (dispatch, getState)=> {
54 | let auth = getState().auth.authenticated;
55 | dispatch({
56 | type: AUTH_UPDATE_PROFILE,
57 | AWAIT_MARKER,
58 | payload: {
59 | updateProfile: Auth.updateProfile(profile, auth.user.uid)
60 | }
61 | });
62 | }
63 | }
64 |
65 | export function getProfile() {
66 | return (dispatch, getState)=> {
67 | let auth = getState().auth.authenticated;
68 | if (!auth.profile.updated_at) {
69 | dispatch({
70 | type: AUTH_GET_PROFILE,
71 | AWAIT_MARKER,
72 | payload: {
73 | getProfile: Auth.getProfile(auth.user.uid)
74 | }
75 | });
76 | }
77 | }
78 | }
79 |
80 | export function checkToken() {
81 | return (dispatch) => {
82 | dispatch({
83 | type: AUTH_CHECK_TOKEN,
84 | AWAIT_MARKER,
85 | payload: {
86 | userFromToken: Auth.isAuthenticated()
87 | }
88 | })
89 | }
90 | }
91 |
92 | export function authLogout() {
93 | return (dispatch) => {
94 | dispatch({type: AUTH_LOGOUT});
95 | Auth.logout();
96 | }
97 | }
--------------------------------------------------------------------------------
/app/redux/actions/AwaitAction.js:
--------------------------------------------------------------------------------
1 | export const AWAIT_RESET = 'await reset';
2 |
3 | export function resetAwait(items) {
4 | return (dispatch)=> {
5 | dispatch({
6 | type: AWAIT_RESET,
7 | items: items
8 | });
9 | }
10 | }
--------------------------------------------------------------------------------
/app/redux/actions/PostAction.js:
--------------------------------------------------------------------------------
1 | import {AWAIT_MARKER} from 'redux-await';
2 | import PostApi from '../../api/post/index';
3 | import {postsList} from '../../configs/index';
4 |
5 | export const POST_CREATE = 'post create';
6 | export const POST_UPDATE = 'post update';
7 | export const POST_LISTS = 'posts lists';
8 | export const POST_LISTS_LOAD_MORE = 'post load more';
9 | export const POST_VIEW = 'post view';
10 | export const POST_EDIT = 'post edit';
11 | export const POST_DELETE = 'post delete';
12 | export const POST_RESET = 'post reset';
13 |
14 | export function createPost(post) {
15 | return (dispatch, getState)=> {
16 | dispatch({
17 | type: POST_CREATE,
18 | AWAIT_MARKER,
19 | payload: {
20 | createPost: PostApi.createPost(post, getState().auth.authenticated.user.uid)
21 | }
22 | });
23 | }
24 | }
25 |
26 | export function updatePost(post, post_id) {
27 | return (dispatch) => {
28 | dispatch({
29 | type: POST_UPDATE,
30 | AWAIT_MARKER,
31 | payload: {
32 | updatePost: PostApi.updatePost(post, post_id)
33 | }
34 | })
35 | }
36 | }
37 |
38 | export function getPostsList() {
39 | return (dispatch) => {
40 | dispatch({
41 | type: POST_LISTS,
42 | AWAIT_MARKER,
43 | payload: {
44 | getPosts: PostApi.getPostsList()
45 | }
46 | });
47 | }
48 | }
49 |
50 | export function getPostView(id) {
51 | return (dispatch) => {
52 | dispatch({
53 | type: POST_VIEW,
54 | AWAIT_MARKER,
55 | payload: {
56 | getPost: PostApi.getPost(id)
57 | }
58 | })
59 | }
60 | }
61 |
62 | export function deletePost(id){
63 | return (dispatch) => {
64 | dispatch({
65 | type: POST_DELETE,
66 | AWAIT_MARKER,
67 | payload: {
68 | deletePost: PostApi.deletePost(id)
69 | }
70 | })
71 | }
72 | }
73 |
74 | export function loadMorePosts(loadMore = postsList.perPage){
75 | return (dispatch) => {
76 | dispatch({
77 | type: POST_LISTS_LOAD_MORE,
78 | loadMore: loadMore
79 | })
80 | }
81 | }
82 |
83 | export function resetCurrentPost(){
84 | return (dispatch) => {
85 | dispatch({
86 | type: POST_RESET
87 | })
88 | }
89 | }
90 |
91 | //export function getPostsList() {
92 | // return (dispatch, getState) => {
93 | // dispatch({type: POST_LISTS_FETCHING});
94 | // PostApi.getPostsList().then(posts => {
95 | // dispatch({type: POST_LISTS_COMPLETED, payload: {posts}})
96 | // }).catch(error => {
97 | // dispatch({type: POST_LISTS_FAILED, error: error});
98 | // })
99 | // }
100 | //}
--------------------------------------------------------------------------------
/app/redux/containers/CompareUID.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from '../../utils/reduxAwait';
3 |
4 | class CompareUID extends Component {
5 | render() {
6 | return (
7 |
8 | {this.props.compare_uid == this.props.uid && this.props.children}
9 |
10 | );
11 | }
12 | }
13 |
14 | CompareUID.propTypes = {
15 | compare_uid: PropTypes.string
16 | }
17 |
18 | const mapStateToProps = (state) => {
19 | return {
20 | uid: state.authenticated.user.uid
21 | }
22 | }
23 |
24 | export default connect(mapStateToProps, null)(CompareUID)
25 |
--------------------------------------------------------------------------------
/app/redux/containers/member/LoginContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {bindActionCreators, bindActionCreator} from 'redux';
3 | import {reduxForm} from 'redux-form';
4 | import validator from 'validator';
5 | import {setTitle, reduxAwait} from '../../../utils/index';
6 | import {authLogin} from '../../actions/AuthAction';
7 | import Login from '../../../components/pages/member/Login';
8 |
9 | class LoginContainer extends React.Component {
10 | onSubmit(...args) {
11 | this.props.authLogin(...args);
12 | }
13 | componentDidMount(){
14 | setTitle('Login');
15 | }
16 | render() {
17 | return (
18 |
19 | )
20 | }
21 | }
22 |
23 | const fields = ['email', 'password'];
24 |
25 | const validate = (values) => {
26 | const {email, password} = values;
27 | const errors = {};
28 |
29 | if (!email) errors.email = 'Required';
30 | else if (!validator.isEmail(email)) errors.email = 'Not valid email';
31 |
32 | if (!password) errors.password = 'Required';
33 |
34 | return errors;
35 | }
36 |
37 | const mapStateToProps = (state)=> {
38 | return {
39 | guest: state.auth.authenticated.guest
40 | }
41 | }
42 |
43 | const mapDispatchToProps = (dispatch)=> {
44 | return bindActionCreators({authLogin}, dispatch)
45 | }
46 |
47 | let LoginForm = reduxForm({
48 | form: 'Login',
49 | fields,
50 | validate
51 | })(LoginContainer);
52 |
53 | export default reduxAwait.connect(mapStateToProps, mapDispatchToProps)(LoginForm);
--------------------------------------------------------------------------------
/app/redux/containers/member/LogoutContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {authLogout} from '../../actions/AuthAction';
4 | import {hashHistory} from 'react-router';
5 |
6 | class Logout extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.props.dispatch(authLogout());
10 | }
11 | render() {
12 | return (
13 |
14 | Logout
15 |
16 | )
17 | }
18 | }
19 |
20 | export default connect()(Logout);
21 |
--------------------------------------------------------------------------------
/app/redux/containers/member/ProfileContainer.js:
--------------------------------------------------------------------------------
1 | // Packages
2 | import React, {Component} from 'react';
3 | import {bindActionCreators} from 'redux';
4 | import {reduxForm} from 'redux-form';
5 | import validator from 'validator';
6 |
7 | //Components
8 | import {setTitle, reduxAwait} from '../../../utils/index';
9 | import {updateProfile, getProfile} from '../../actions/AuthAction';
10 | import {resetAwait} from '../../actions/AwaitAction';
11 | import Profile from '../../../components/pages/member/Profile';
12 |
13 | class ProfileContainer extends Component {
14 | componentDidMount() {
15 | this.props.getProfile();
16 | this.props.resetAwait(['updateProfile']);
17 | setTitle('My profile');
18 | }
19 |
20 | render() {
21 | return (
22 |
23 | )
24 | }
25 | }
26 |
27 | const mapStateToProps = (state)=> {
28 | return {
29 | initialValues: state.auth.authenticated.profile
30 | }
31 | }
32 |
33 | const mapDispatchToProps = (dispatch)=> {
34 | return bindActionCreators({updateProfile, getProfile, resetAwait}, dispatch);
35 | }
36 |
37 | const fields = ['first_name', 'last_name', 'description'];
38 |
39 | const validate = values => {
40 | const errors = {};
41 | const {first_name, last_name} = values;
42 | fields.map((field) => {
43 | if (!values[field]) {
44 | errors[field] = `Required`;
45 | }
46 | else {
47 | switch (field) {
48 | case 'first_name':
49 | if (!validator.isAlpha(first_name)) {
50 | errors.first_name = 'First name only string';
51 | }
52 | break;
53 | case 'last_name':
54 | if (!validator.isAlpha(last_name)) {
55 | errors.last_name = 'Last name only string';
56 | }
57 | break;
58 | }
59 | }
60 | });
61 | return errors;
62 | }
63 |
64 | let profileForm = reduxForm({
65 | form: 'initializing',
66 | fields,
67 | validate
68 | })(ProfileContainer);
69 |
70 | export default reduxAwait.connect(mapStateToProps, mapDispatchToProps)(profileForm)
--------------------------------------------------------------------------------
/app/redux/containers/member/RegisterContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {bindActionCreators} from 'redux';
3 | import {reduxForm} from 'redux-form';
4 | import validator from 'validator';
5 | import Register from '../../../components/pages/member/Register';
6 | import {authRegister, authLogin} from '../../actions/AuthAction';
7 | import {setTitle, reduxAwait} from '../../../utils/index';
8 |
9 | class RegisterContainer extends React.Component {
10 | onSubmit(...args) {
11 | this.props.authRegister(...args);
12 | }
13 |
14 | componentDidUpdate() {
15 | const {awaitStatuses, registerForm: {email, password}} = this.props;
16 | if (awaitStatuses.userRegister == 'success' && awaitStatuses.userLogin != 'pending') {
17 | this.props.authLogin(email, password);
18 | }
19 | }
20 |
21 | componentDidMount(){
22 | setTitle('Register');
23 | }
24 |
25 | render() {
26 | return (
27 |
28 | )
29 | }
30 | }
31 |
32 | const fields = ['email', 'first_name', 'last_name', 'password', 're_password'];
33 |
34 | const validate = values => {
35 | const errors = {};
36 | const {email, password, re_password, first_name, last_name} = values;
37 |
38 | fields.map((field) => {
39 | if (!values[field]) {
40 | errors[field] = `Required`;
41 | }
42 | else {
43 | switch (field) {
44 | case 'email':
45 | if (!validator.isEmail(email)) {
46 | errors.email = 'Not valid email';
47 | }
48 | ;
49 | break;
50 | case 'first_name':
51 | if (!validator.isAlpha(first_name)) {
52 | errors.first_name = 'First name only string';
53 | }
54 | break;
55 | case 'last_name':
56 | if (!validator.isAlpha(last_name)) {
57 | errors.last_name = 'Last name only string';
58 | }
59 | break;
60 | case 'password':
61 | if (!validator.isLength(password, {min: 6})) {
62 | errors.password = 'Password\'s min length 6'
63 | }
64 | break;
65 | case 're_password':
66 | if (re_password != password) {
67 | errors.re_password = 'Password\' not correct';
68 | }
69 | break;
70 | }
71 | }
72 | });
73 | return errors;
74 | }
75 |
76 | const mapStateToProps = (state)=> {
77 | return {
78 | registerForm: state.auth.register,
79 | guest: state.auth.authenticated.guest,
80 | }
81 | }
82 |
83 | const mapDispatchToProps = (dispatch)=> {
84 | return bindActionCreators({authRegister, authLogin}, dispatch)
85 | }
86 |
87 | let registerForm = reduxForm({
88 | form: 'formRegister',
89 | fields,
90 | validate
91 | })(RegisterContainer);
92 |
93 | export default reduxAwait.connect(mapStateToProps, mapDispatchToProps)(registerForm);
--------------------------------------------------------------------------------
/app/redux/containers/member/index.js:
--------------------------------------------------------------------------------
1 | import LoginContainer from './LoginContainer';
2 | import ProfileContainer from './ProfileContainer';
3 | import RegisterContainer from './RegisterContainer';
4 | import LogoutContainer from './LogoutContainer';
5 |
6 | export default {
7 | LoginContainer,
8 | ProfileContainer,
9 | RegisterContainer,
10 | LogoutContainer
11 | };
12 |
13 | export {LoginContainer, ProfileContainer, RegisterContainer, LogoutContainer}
--------------------------------------------------------------------------------
/app/redux/containers/posts/CreatePostContainer.js:
--------------------------------------------------------------------------------
1 | // Packages
2 | import React, {Component} from 'react';
3 | import {bindActionCreators} from 'redux';
4 | import {reduxForm} from 'redux-form';
5 | import {pushState, hashHistory} from 'react-router';
6 |
7 | // Components
8 | import {setTitle, reduxAwait} from '../../../utils/index';
9 | import {createPost} from '../../actions/PostAction';
10 | import {resetAwait} from '../../actions/AwaitAction';
11 | import PostForm from '../../../components/pages/posts/PostForm';
12 |
13 |
14 | class CreatePostContainer extends Component {
15 | onSubmit(post) {
16 | this.props.createPost(post);
17 | }
18 |
19 | componentDidUpdate() {
20 | const {awaitStatuses, keyAwait, postCreated} = this.props;
21 | if (awaitStatuses[keyAwait] == 'success' && awaitStatuses.createPost) {
22 | hashHistory.push(`/posts/edit/${postCreated.id}`);
23 | }
24 | setTitle(`Create post`);
25 | }
26 |
27 | componentDidMount() {
28 | this.props.resetAwait([this.props.keyAwait]);
29 | }
30 |
31 | render() {
32 | return (
33 |
34 | )
35 | }
36 | }
37 |
38 | const mapStateToProps = (state)=> {
39 | return {
40 | postCreated: state.posts.currentPost,
41 | formType: 'create',
42 | keyAwait: 'createPost'
43 | }
44 | }
45 |
46 | const mapDispatchToProps = (dispatch)=> {
47 | return bindActionCreators({createPost, resetAwait}, dispatch);
48 |
49 | }
50 |
51 | const validate = (values) => {
52 | let errors = {};
53 | fields.map((field) => {
54 | if (!values[field]) {
55 | errors[field] = `${field} is required`;
56 | }
57 | });
58 | return errors;
59 | }
60 |
61 | const fields = ['title', 'description', 'content'];
62 |
63 | let createForm = reduxForm({
64 | form: 'CreatePost',
65 | fields,
66 | validate
67 | })(CreatePostContainer);
68 |
69 | export default reduxAwait.connect(mapStateToProps, mapDispatchToProps)(createForm);
--------------------------------------------------------------------------------
/app/redux/containers/posts/EditPostContainer.js:
--------------------------------------------------------------------------------
1 | // Import packages
2 | import React, {Component} from 'react';
3 | import {bindActionCreators} from 'redux';
4 | import {reduxForm} from 'redux-form';
5 | import {pushState, hashHistory} from 'react-router';
6 |
7 | // Import components
8 | import {setTitle, reduxAwait} from '../../../utils/index';
9 | import {getPostView, updatePost, deletePost} from '../../actions/PostAction';
10 | import {resetAwait} from '../../actions/AwaitAction';
11 | import PostForm from '../../../components/pages/posts/PostForm';
12 |
13 |
14 | export class EditPostContainer extends Component {
15 | onSubmit(post) {
16 | this.props.updatePost(post, this.props.post_id);
17 | }
18 |
19 | onDelete() {
20 | let s_confirm = confirm('Are you sure?')
21 | if(s_confirm){
22 | this.props.deletePost(this.props.post_id);
23 | }
24 | }
25 |
26 | componentDidMount() {
27 | this.props.getPostView(this.props.post_id);
28 | this.props.resetAwait(['updatePost', 'deletePost']);
29 | }
30 |
31 | componentDidUpdate() {
32 | if (this.props.awaitStatuses.deletePost == 'success') {
33 | hashHistory.push('posts');
34 | }
35 | setTitle(`Edit post ${this.props.post.title ? this.props.post.title : ''}`)
36 | }
37 |
38 | render() {
39 | return (
40 |
43 | )
44 | }
45 | }
46 |
47 | const mapStateToProps = (state, ownProps)=> {
48 | return {
49 | initialValues: state.posts.currentPost,
50 | post: state.posts.currentPost,
51 | post_id: ownProps.params.id,
52 | formType: 'edit',
53 | keyAwait: "updatePost"
54 | }
55 | }
56 |
57 | const mapDispatchToProps = (dispatch, state)=> {
58 | return bindActionCreators({updatePost, getPostView, deletePost, resetAwait}, dispatch);
59 |
60 | }
61 |
62 | const validate = (values) => {
63 | let errors = {};
64 | fields.map((field) => {
65 | if (!values[field]) {
66 | errors[field] = `${field} is required`;
67 | }
68 | });
69 | return errors;
70 | }
71 |
72 | const fields = ['title', 'description', 'content'];
73 |
74 | let editorForm = reduxForm({
75 | form: 'initializing',
76 | fields,
77 | validate
78 | })(EditPostContainer);
79 |
80 | export default reduxAwait.connect(mapStateToProps, mapDispatchToProps)(editorForm);
--------------------------------------------------------------------------------
/app/redux/containers/posts/PostViewContainer.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {bindActionCreators} from 'redux';
3 | import {setTitle, reduxAwait} from '../../../utils/index';
4 | import {PostView} from '../../../components/pages/posts/index';
5 | import {getPostView, resetCurrentPost} from '../../actions/PostAction';
6 |
7 | class PostViewContainer extends Component {
8 | constructor() {
9 | super(...arguments);
10 | this.state = {
11 | isAuthor: false
12 | }
13 | }
14 |
15 | componentWillUnmount() {
16 | this.props.resetCurrentPost();
17 | }
18 |
19 | componentWillReceiveProps(nextProps) {
20 | if (this.props.post_id != nextProps.post_id) {
21 | this.props.getPostView(nextProps.post_id);
22 | }
23 | }
24 |
25 | componentDidUpdate() {
26 | if (this.props.post.uid == this.props.uid && !this.state.isAuthor && this.props.post.uid) {
27 | this.setState({isAuthor: true});
28 | }
29 | if (this.props.post.title) {
30 | setTitle(this.props.post.title);
31 | }
32 | }
33 |
34 | componentDidMount() {
35 | this.props.getPostView(this.props.post_id);
36 | }
37 |
38 | render() {
39 | return (
40 |
41 | )
42 | }
43 | }
44 |
45 | const mapStateToProps = (state, ownProps)=> {
46 | return {
47 | uid: state.auth.authenticated.user.uid,
48 | post: state.posts.currentPost,
49 | post_id: ownProps.params.id
50 | }
51 | }
52 |
53 | const mapDispatchToProps = (dispatch)=> {
54 | return bindActionCreators({getPostView, resetCurrentPost}, dispatch);
55 | }
56 |
57 | export default reduxAwait.connect(mapStateToProps, mapDispatchToProps)(PostViewContainer);
58 |
--------------------------------------------------------------------------------
/app/redux/containers/posts/PostsListContainer.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {bindActionCreators} from 'redux';
3 | import {setTitle, reduxAwait} from '../../../utils/index';
4 | import {getPostsList, loadMorePosts} from '../../actions/PostAction';
5 | import {PostsList} from '../../../components/pages/posts/index';
6 | import {postsList as postsListConfig} from '../../../configs/index';
7 |
8 | class PostsListContainer extends Component{
9 | componentDidMount(){
10 | this.props.getPostsList();
11 | setTitle('Posts List');
12 | }
13 |
14 | render(){
15 | return (
16 |
17 | )
18 | }
19 | }
20 |
21 | const mapStateToProps = (state)=> {
22 | return {
23 | posts: state.posts.lists,
24 | currentItems: state.posts.currentItems
25 | }
26 | }
27 |
28 | const mapDispatchToProps = (dispatch)=> {
29 | return bindActionCreators({getPostsList, loadMore: loadMorePosts}, dispatch)
30 | }
31 |
32 | export default reduxAwait.connect(mapStateToProps, mapDispatchToProps)(PostsListContainer);
--------------------------------------------------------------------------------
/app/redux/containers/posts/index.js:
--------------------------------------------------------------------------------
1 | import CreatePostContainer from './CreatePostContainer';
2 | import EditPostContainer from './EditPostContainer';
3 | import PostsListContainer from './PostsListContainer';
4 | import PostViewContainer from './PostViewContainer';
5 |
6 | export default {
7 | CreatePostContainer,
8 | PostsListContainer,
9 | PostViewContainer,
10 | EditPostContainer
11 | }
12 |
13 | export {CreatePostContainer, PostsListContainer, PostViewContainer, EditPostContainer}
--------------------------------------------------------------------------------
/app/redux/containers/requireAuth.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {bindActionCreators} from 'redux';
3 | import {connect} from 'react-redux';
4 | import {pushState, hashHistory} from 'react-router';
5 | import {checkToken} from '../actions/AuthAction';
6 | import Auth from '../../api/auth/index';
7 |
8 | export const REDIRECT_IF_GUEST = 'redirect if guest';
9 | export const REDIRECT_IF_AUTHENTICATED = 'redirect if authenticated';
10 |
11 | /**
12 | * Require auth (redirect if authenticated, or not authenticated)
13 | * @param Component | React Component
14 | * @param redirectIfNotAuthenticated | = true => redirect if not auth | false => redirect if atuh
15 | * @param redirect | link redirect if not match Authenticated check
16 | * @returns {*}
17 | */
18 | export function requireAuth(Component, redirectCheck = REDIRECT_IF_GUEST, redirect = '/auth/login') {
19 | class AuthenticatedComponent extends React.Component {
20 | constructor() {
21 | super(...arguments);
22 | this.checkTokenInterval = '';
23 | }
24 |
25 | checkAuth(guest) {
26 | switch (redirectCheck) {
27 | case REDIRECT_IF_GUEST:
28 | if (guest) {
29 | let redirectAfterLogin = this.props.location.pathname;
30 | hashHistory.push(`${redirect}?next=${redirectAfterLogin}`);
31 | }
32 | break;
33 | case REDIRECT_IF_AUTHENTICATED:
34 | if (!guest) {
35 | let nextUrl = this.props.location.query.next;
36 | if (nextUrl) {
37 | redirect = nextUrl;
38 | }
39 | hashHistory.push(redirect);
40 | }
41 | }
42 | }
43 |
44 | componentDidUpdate() {
45 | this.checkAuth(this.props.guest);
46 | }
47 |
48 | componentDidMount() {
49 | this.checkAuth(this.props.guest);
50 | }
51 |
52 | render() {
53 | let component;
54 | switch (redirectCheck) {
55 | case REDIRECT_IF_GUEST:
56 | component = !this.props.guest ? : null;
57 | break;
58 | case REDIRECT_IF_AUTHENTICATED:
59 | component = this.props.guest ? : null;
60 | break;
61 | }
62 | return (
63 |
64 | {component}
65 |
66 | )
67 | }
68 | }
69 |
70 | const mapStateToProps = (state)=>({
71 | guest: state.auth.authenticated.guest
72 | });
73 |
74 | const mapDispatchToProps = (dispatch)=>({
75 | actions: bindActionCreators({checkToken: checkToken}, dispatch)
76 | })
77 |
78 | return connect(mapStateToProps, mapDispatchToProps)(AuthenticatedComponent);
79 | }
80 |
81 | export function redirectIfGuest(Component, redirect = "/auth/login") {
82 | return requireAuth(Component, REDIRECT_IF_GUEST, redirect)
83 | }
84 |
85 | export function redirectIfAuthenticated(Component, redirect = "/") {
86 | return requireAuth(Component, REDIRECT_IF_AUTHENTICATED, redirect);
87 | }
88 |
--------------------------------------------------------------------------------
/app/redux/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | AUTH_LOGIN, AUTH_LOGOUT,
3 | AUTH_REGISTER,
4 | AUTH_GET_PROFILE,
5 | AUTH_UPDATE_PROFILE,
6 | AUTH_CHECK_TOKEN
7 | } from '../actions/AuthAction';
8 | import update from 'react-addons-update';
9 | import {createReducer} from 'redux-create-reducer';
10 |
11 | /*let initinalState = {
12 | isAuthenticated:
13 | }*/
14 |
15 | const getInitialState = () => {
16 | return {
17 | authenticated: {
18 | guest: true,
19 | user: {},
20 | profile: {
21 | updated_at: '',
22 | }
23 | },
24 | register: {
25 | email: '',
26 | password: ''
27 | },
28 | logout: {
29 | isFetching: false,
30 | }
31 | }
32 | }
33 |
34 | export default createReducer(getInitialState(), {
35 | [AUTH_LOGIN](state, action){
36 | localStorage.setItem('authenticated', JSON.stringify(action.payload.userLogin));
37 | return update(state, {
38 | authenticated: {
39 | guest: {$set: false},
40 | user: {$set: action.payload.userLogin},
41 | }
42 | });
43 | },
44 |
45 | [AUTH_REGISTER](state, action){
46 | return update(state, {
47 | register: {$set: action.payload.userRegister}
48 | })
49 | },
50 |
51 | [AUTH_GET_PROFILE](state, action){
52 | let profile = {...action.payload.getProfile, updated_at: new Date().getTime()}
53 | return update(state, {
54 | authenticated: {
55 | profile: {$set: profile}
56 | }
57 | });
58 | },
59 |
60 | [AUTH_UPDATE_PROFILE](state, action){
61 | let profile = {...action.payload.updateProfile, updated_at: new Date().getTime()};
62 |
63 | return update(state, {
64 | authenticated: {
65 | profile: {$set: profile}
66 | }
67 | });
68 | },
69 |
70 | [AUTH_CHECK_TOKEN](state, action){
71 | return update(state, {
72 | authenticated: {
73 | guest: {$set: false},
74 | user: {$set: action.payload.userFromToken}
75 | }
76 | });
77 | },
78 |
79 | [AUTH_LOGOUT](state, action){
80 | return update(state, {$set: getInitialState()});
81 | }
82 | });
83 |
--------------------------------------------------------------------------------
/app/redux/reducers/await.js:
--------------------------------------------------------------------------------
1 | import {reducer as awaitReducer} from 'redux-await';
2 | import {AWAIT_RESET} from '../actions/AwaitAction';
3 |
4 | export default function await(state = {statuses: [], errors: []}, action) {
5 | const {statuses, errors} = awaitReducer(state, action);
6 | if (action.type === AWAIT_RESET) {
7 | action.items.forEach(item => {
8 | statuses[item] = null;
9 | errors[item] = null;
10 | })
11 | }
12 | return {statuses, errors};
13 | }
--------------------------------------------------------------------------------
/app/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import auth from './auth';
3 | import posts from './posts';
4 | import {reducer as formReducer} from 'redux-form';
5 | import {reducer as awaitReducer} from 'redux-await';
6 | import await from './await';
7 |
8 | export default combineReducers({
9 | auth,
10 | posts,
11 | form: formReducer,
12 | await
13 | })
--------------------------------------------------------------------------------
/app/redux/reducers/posts.js:
--------------------------------------------------------------------------------
1 | import {
2 | POST_LISTS, POST_CREATE, POST_VIEW, POST_EDIT, POST_RESET, POST_LISTS_LOAD_MORE
3 | } from '../actions/PostAction';
4 | import update from 'react-addons-update';
5 | import {createReducer} from 'redux-create-reducer';
6 | import {postsList} from '../../configs/index';
7 |
8 | const getInitialState = () => {
9 | return {
10 | lists: [],
11 | currentItems: postsList.perPage, //pagination
12 | currentPost: {
13 | user: {}
14 | }
15 | }
16 | }
17 |
18 | export default createReducer(getInitialState(), {
19 | [POST_LISTS](state, action){
20 | return update(state, {
21 | lists: {$set: action.payload.getPosts}
22 | });
23 | },
24 | [POST_CREATE](state, action){
25 | return update(state, {
26 | currentPost: {$set: action.payload.createPost}
27 | })
28 | },
29 | [POST_VIEW](state, action){
30 | return update(state, {
31 | currentPost: {$set: action.payload.getPost}
32 | });
33 | },
34 | [POST_RESET](state){
35 | return update(state, {
36 | currentPost: {$set: {user: {}}}
37 | });
38 | },
39 | [POST_LISTS_LOAD_MORE](state, action){
40 | return update(state, {
41 | currentItems: {
42 | $apply: (value) => {
43 | let currentItems = value + action.loadMore;
44 | if(currentItems < state.lists.length)
45 | return currentItems;
46 | else return state.lists.length
47 | }
48 | }
49 | })
50 | }
51 | });
--------------------------------------------------------------------------------
/app/redux/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware, compose} from 'redux';
2 | import thunkMiddleware from 'redux-thunk';
3 | import {middleware as awaitMiddleware} from 'redux-await';
4 | import rootReducers from '../reducers/index';
5 |
6 | export default function configureStore(initialState) {
7 | return createStore(
8 | rootReducers,
9 | initialState,
10 | compose(
11 | applyMiddleware(thunkMiddleware, awaitMiddleware),
12 | window.devToolsExtension ? window.devToolsExtension() : f => f
13 | )
14 | )
15 | }
--------------------------------------------------------------------------------
/app/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Router, Route, IndexRoute, hashHistory} from 'react-router';
3 | import {redirectIfGuest, redirectIfAuthenticated} from '../redux/containers/requireAuth';
4 |
5 | import AppMaster from '../components/layouts/master/AppMaster';
6 |
7 | import {LoginContainer, ProfileContainer, RegisterContainer, LogoutContainer} from '../redux/containers/member/index';
8 | import {CreatePostContainer, PostsListContainer, PostViewContainer, EditPostContainer} from '../redux/containers/posts/index';
9 |
10 |
11 |
12 | export default () => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
--------------------------------------------------------------------------------
/app/stylesheets/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix() {
2 | &:before,
3 | &:after {
4 | content: " "; // 1
5 | display: table; // 2
6 | }
7 | &:after {
8 | clear: both;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/stylesheets/_variables.scss:
--------------------------------------------------------------------------------
1 | $bg-primary-color: #db4c3f;
2 | $text-header: #ffffff;
3 | $primary: #424242;
4 | $btn-white: #ffffff;
5 | $btn-orange: #e67e22;
6 | $btn-orange-hover: #d35400;
7 | $a-hover: #fc605c;
8 | $font-montserrat: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif;
9 | $font-default: "Lora"
10 |
--------------------------------------------------------------------------------
/app/stylesheets/pages/post_lists.scss:
--------------------------------------------------------------------------------
1 | .post-lists {
2 | .post-item {
3 | border-left: 3px solid #eeeeee;
4 | padding-left: 20px;
5 | padding: 5px 20px;
6 | margin-bottom: 10px;
7 | -webkit-transition: all .3s linear;
8 | -moz-transition: all .3s linear;
9 | -ms-transition: all .3s linear;
10 | -o-transition: all .3s linear;
11 | transition: all .3s linear;
12 | h4.title {
13 | margin-top: 0px;
14 | margin-bottom: 0px;
15 | font-size: 16px;
16 | a {
17 | color: lighten($bg-primary-color, 10);
18 | -webkit-transition: all .3s linear;
19 | -moz-transition: all .3s linear;
20 | -ms-transition: all .3s linear;
21 | -o-transition: all .3s linear;
22 | transition: all .3s linear;
23 | &:hover {
24 | text-decoration: none;
25 | color: $bg-primary-color
26 | }
27 | }
28 | }
29 | .description {
30 | line-height: 1.7em;
31 | margin-bottom: 0px;
32 | }
33 | .user {
34 | color: #c7c7c7;
35 | font-size: 12px;
36 | }
37 | &:hover {
38 | border-left: 3px solid lighten($bg-primary-color, 10);
39 | }
40 | }
41 | }
42 |
43 | .post-view {
44 | & h1.title {
45 | margin-bottom: 5px !important;
46 | }
47 | .meta {
48 | display: block;
49 | width: 100%;
50 | @include clearfix();
51 | ul {
52 | padding-left: 0px;
53 | li {
54 | float: left;
55 | list-style: none;
56 | padding-right: 30px;
57 | font-size: 12px;
58 | color: #c7c7c7;
59 | a{
60 | &:hover{text-decoration: none; cursor: pointer}
61 | }
62 | }
63 | }
64 | }
65 | .content{
66 | line-height: 1.7em;
67 | font-size: 14px;
68 | margin-top: 20px;
69 | div[data-block=true]{
70 | margin-top: 15px;
71 | margin-bottom: 15px;
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/app/stylesheets/partials/buttons.scss:
--------------------------------------------------------------------------------
1 | @import "../variables";
2 |
3 | @mixin button_vndev($color, $color-text:#ffffff) {
4 | background: $color;
5 | box-shadow: 0px 3px 0px 0px darken($color, 10%);
6 | border-radius: 2px;
7 | -webkit-transition: all .3s linear;
8 | -moz-transition: all .3s linear;
9 | -ms-transition: all .3s linear;
10 | -o-transition: all .3s linear;
11 | transition: all .3s linear;
12 | color: $color-text;
13 | &:hover, &:focus {
14 | background: lighten($color, 6%);
15 | color: $color-text;
16 | outline: none;
17 | }
18 | }
19 |
20 | .btn-red {
21 | @include button_vndev(#e7543d);
22 | }
23 |
24 | .btn-blue {
25 | @include button_vndev(#39add1);
26 | }
27 |
28 | .btn-orange {
29 | @include button_vndev(#eb7728);
30 | }
31 |
32 | .btn-purple {
33 | @include button_vndev(#9e4d83);
34 | }
35 |
36 | .btn-green {
37 | @include button_vndev(#5cb860);
38 | }
39 |
40 | .btn-yellow {
41 | @include button_vndev(#ebba10)
42 | }
43 |
44 | .btn-blue-dark {
45 | @include button_vndev(#3e474f)
46 | }
47 |
48 | .btn-circle {
49 | width: 70px;
50 | height: 70px;
51 | margin-top: 15px;
52 | padding: 7px 16px;
53 | border: 2px solid white;
54 | border-radius: 100% !important;
55 | font-size: 40px;
56 | color: white;
57 | position: relative;
58 | z-index: 9999;
59 | background: transparent;
60 | -webkit-transition: background .3s ease-in-out;
61 | -moz-transition: background .3s ease-in-out;
62 | transition: background .3s ease-in-out;
63 | &:hover,
64 | &:focus {
65 | outline: none;
66 | color: white;
67 | background: rgba(white, 0.1);
68 | }
69 | i.animated {
70 | -webkit-transition-property: -webkit-transform;
71 | -webkit-transition-duration: 1s;
72 | -moz-transition-property: -moz-transform;
73 | -moz-transition-duration: 1s;
74 | }
75 | &:hover {
76 | i.animated {
77 | -webkit-animation-name: pulse;
78 | -moz-animation-name: pulse;
79 | -webkit-animation-duration: 1.5s;
80 | -moz-animation-duration: 1.5s;
81 | -webkit-animation-iteration-count: infinite;
82 | -moz-animation-iteration-count: infinite;
83 | -webkit-animation-timing-function: linear;
84 | -moz-animation-timing-function: linear;
85 | }
86 | }
87 | }
88 |
89 | @-webkit-keyframes pulse {
90 | from {
91 | -webkit-transform: scale(1);
92 | transform: scale(1);
93 | }
94 |
95 | 50% {
96 | -webkit-transform: scale(1.2);
97 | transform: scale(1.2);
98 | }
99 |
100 | 100% {
101 | -webkit-transform: scale(1);
102 | transform: scale(1);
103 | }
104 | }
105 |
106 | @-moz-keyframes pulse {
107 | from {
108 | -moz-transform: scale(1);
109 | transform: scale(1);
110 | }
111 |
112 | 50% {
113 | -moz-transform: scale(1.2);
114 | transform: scale(1.2);
115 | }
116 |
117 | 100% {
118 | -moz-transform: scale(1);
119 | transform: scale(1);
120 | }
121 | }
122 |
123 | .btn-white {
124 | border: 1px solid $btn-white;
125 | color: $btn-white;
126 | background-color: transparent;
127 | &:hover,
128 | &:focus {
129 | border: 1px solid $primary;
130 | outline: none;
131 | color: #ffffff;
132 | background-color: $primary;
133 | }
134 | }
135 |
136 | .btn-black {
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/app/stylesheets/partials/header.scss:
--------------------------------------------------------------------------------
1 | @import "./buttons";
2 | .navbar-custom {
3 | background: $bg-primary-color;
4 | border: 0px;
5 | font-size: 14px;
6 | border-bottom: 0px;
7 | border-bottom: 1px solid #eeeeee;
8 | margin-bottom: 0px;
9 | border-radius: 0px;
10 | min-height: 40px;
11 | .navbar-brand {
12 | padding: 8px;
13 | position: relative;
14 | height: 40px !important;
15 | img {
16 | height: 20px;
17 | margin-top: 3px;
18 | }
19 | .underline{
20 | position: absolute;
21 | top: 6px;
22 | left: 23px;
23 | font-weight: 700;
24 | color:#ffffff
25 | }
26 | }
27 | .navbar-nav {
28 | & > li {
29 | &.dropdown > a {
30 | background: transparent;
31 | &:hover, &:focus {
32 | background: transparent;
33 | }
34 | }
35 | & > a {
36 | -webkit-transition: all .3s linear;
37 | -moz-transition: all .3s linear;
38 | -ms-transition: all .3s linear;
39 | -o-transition: all .3s linear;
40 | transition: all .3s linear;
41 | font-size: 14px;
42 | color: $text-header;
43 | background: transparent;
44 | font-family: 'Raleway';
45 | @media (min-width: 768px) {
46 | padding-top: 12px;
47 | padding-bottom: 12px;
48 | }
49 | &:hover, &:focus {
50 | background: lighten($bg-primary-color, 6%);
51 | color: #ffffff;
52 | //color: #ef6c00;
53 | //background: transparent !important;
54 | outline: none;
55 | }
56 | &.btn {
57 | color: #ffffff;
58 | padding-top: 5px;
59 | padding-bottom: 5px;
60 | margin-top: 10px;
61 | &.login {
62 | @extend .btn-orange;
63 | }
64 | }
65 | }
66 | .dropdown-menu {
67 | //position: relative;
68 | padding: 0px 0px;
69 | & > li > a{
70 | padding: 5px 20px;
71 | }
72 | &:before {
73 | width: 0px;
74 | height: 0px;
75 | border-left: 7px solid transparent;
76 | border-right: 7px solid transparent;
77 | border-bottom: 8px solid #ffffff;
78 | position: absolute;
79 | top: -8px;
80 | left: 20px;
81 | content: '';
82 | }
83 | &.cart {
84 | .cart-products {
85 | height: 300px;
86 | .product {
87 | width: 400px;
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 | &.navbar-remove-fixed {
95 | position: absolute !important;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/app/stylesheets/partials/loading.scss:
--------------------------------------------------------------------------------
1 | .loading-overlay{
2 | position: absolute;
3 | top: 0px;
4 | left: 0px;
5 | width: 100%;
6 | height: 100%;
7 | z-index: 999;
8 | background: rgba(255,255,255,.8);
9 | }
10 | .mikepad-loading {
11 | width : 150px;
12 | position: absolute;
13 | top: 50%;
14 | left : 50%;
15 | -webkit-transform: translateY(-50%) translateX(-50%);
16 | -moz-transform: translateY(-50%) translateX(-50%);
17 | -o-transform: translateY(-50%) translateX(-50%);
18 | transform: translateY(-50%) translateX(-50%);
19 | }
20 |
21 | .mikepad-loading .binding {
22 | content : '';
23 | width : 27px;
24 | height : 6px;
25 | border : 2px solid #E15958;
26 | margin : 0 auto;
27 | }
28 |
29 | .mikepad-loading .pad {
30 | width : 27px;
31 | height : 26px;
32 | border : 2px solid #E15958;
33 | border-top : 0;
34 | padding : 6px;
35 | margin : 0 auto;
36 | }
37 |
38 | .mikepad-loading .line {
39 | width : 5px;
40 | margin-top : 4px;
41 | border-top : 2px solid #E15958;
42 | opacity : 0;
43 | -webkit-animation : writeline 3s infinite ease-in;
44 | -moz-animation : writeline 3s infinite ease-in;
45 | -o-animation : writeline 3s infinite ease-in;
46 | animation : writeline 3s infinite ease-in;
47 | }
48 |
49 | .mikepad-loading .line:first-child {
50 | margin-top : 0;
51 | }
52 |
53 | .mikepad-loading .line.line1 {
54 | -webkit-animation-delay: 0s;
55 | -moz-animation-delay: 0s;
56 | -o-animation-delay: 0s;
57 | animation-delay: 0s;
58 | }
59 |
60 | .mikepad-loading .line.line2 {
61 | -webkit-animation-delay: 0.5s;
62 | -moz-animation-delay: 0.5s;
63 | -o-animation-delay: 0.5s;
64 | animation-delay: 0.5s;
65 | }
66 |
67 | .mikepad-loading .line.line3 {
68 | -webkit-animation-delay: 1s;
69 | -moz-animation-delay: 1s;
70 | -o-animation-delay: 1s;
71 | animation-delay : 1s;
72 | }
73 |
74 | .mikepad-loading .text {
75 | text-align : center;
76 | margin-top : 10px;
77 | font-size : 14px;
78 | color : #E15958;
79 | }
80 |
81 | @-webkit-keyframes writeline {
82 | 0% { width : 0px; opacity: 0; }
83 | 33% { width : 15px; opacity : 1; }
84 | 70% { opacity : 1; }
85 | 100% {opacity : 0; }
86 | }
87 |
88 | @-moz-keyframes writeline {
89 | 0% { width : 0px; opacity: 0; }
90 | 33% { width : 100%; opacity : 1; }
91 | 70% { opacity : 1; }
92 | 100% {opacity : 0; }
93 | }
94 |
95 | @-o-keyframes writeline {
96 | 0% { width : 0px; opacity: 0; }
97 | 33% { width : 100%; opacity : 1; }
98 | 70% { opacity : 1; }
99 | 100% {opacity : 0; }
100 | }
101 |
102 | @keyframes writeline {
103 | 0% { width : 0px; opacity: 0; }
104 | 33% { width : 100%; opacity : 1; }
105 | 70% { opacity : 1; }
106 | 100% {opacity : 0; }
107 | }
--------------------------------------------------------------------------------
/app/stylesheets/partials/margin.scss:
--------------------------------------------------------------------------------
1 | .margin-top-0{
2 | margin-top: 0px;
3 | }
4 |
5 | .margin-top-10{
6 | margin-top: 10px;
7 | }
8 |
9 | .margin-top-15{
10 | margin-top: 15px;
11 | }
12 |
13 | .margin-top-20{
14 | margin-top: 20px;
15 | }
16 |
17 | .margin-top-25{
18 | margin-top: 25px;
19 | }
20 |
21 | .margin-top-30{
22 | margin-top: 30px;
23 | }
24 |
25 | .margin-top-35{
26 | margin-top: 35px;
27 | }
28 |
29 | .margin-top-40{
30 | margin-top: 40px;
31 | }
32 |
33 | .margin-top-45{
34 | margin-top: 45px;
35 | }
36 | .margin-top-50{
37 | margin-top: 50px;
38 | }
--------------------------------------------------------------------------------
/app/stylesheets/partials/text.scss:
--------------------------------------------------------------------------------
1 | .text-green {
2 | color: #27ae60
3 | }
4 |
5 | .text-green-darken{
6 | color: #009688;
7 | }
8 |
9 | .text-blue {
10 | color: #2980b9
11 | }
12 |
13 | .text-red {
14 | color: #e74c3c
15 | }
16 |
17 | .text-orange {
18 | color: #e67e22
19 | }
20 |
21 | .text-purple {
22 | color: #8e44ad
23 | }
24 |
25 | .text-white {
26 | color: white !important;
27 | }
28 |
29 | .text-black-white {
30 | color: #696969 !important;
31 | }
--------------------------------------------------------------------------------
/app/stylesheets/richText.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * @providesModule DraftEditor
3 | * @permanent
4 | */
5 |
6 | /**
7 | * We inherit the height of the container by default
8 | */
9 |
10 | .DraftEditor-root,
11 | .DraftEditor-editorContainer,
12 | .public-DraftEditor-content {
13 | height: inherit;
14 | text-align: initial;
15 | }
16 |
17 | .DraftEditor-root {
18 | position: relative;
19 | }
20 |
21 | /**
22 | * Zero-opacity background used to allow focus in IE. Otherwise, clicks
23 | * fall through to the placeholder.
24 | */
25 |
26 | .DraftEditor-editorContainer {
27 | background-color: rgba(255, 255, 255, 0);
28 | /* Repair mysterious missing Safari cursor */
29 | border-left: 0.1px solid transparent;
30 | position: relative;
31 | z-index: 1;
32 | }
33 |
34 | .public-DraftEditor-block {
35 | position: relative;
36 | }
37 |
38 | .DraftEditor-alignLeft .public-DraftStyleDefault-block {
39 | text-align: left;
40 | }
41 |
42 | .DraftEditor-alignLeft .public-DraftEditorPlaceholder-root {
43 | left: 0;
44 | text-align: left;
45 | }
46 |
47 | .DraftEditor-alignCenter .public-DraftStyleDefault-block {
48 | text-align: center;
49 | }
50 |
51 | .DraftEditor-alignCenter .public-DraftEditorPlaceholder-root {
52 | margin: 0 auto;
53 | text-align: center;
54 | width: 100%;
55 | }
56 |
57 | .DraftEditor-alignRight .public-DraftStyleDefault-block {
58 | text-align: right;
59 | }
60 |
61 | .DraftEditor-alignRight .public-DraftEditorPlaceholder-root {
62 | right: 0;
63 | text-align: right;
64 | }
65 | /**
66 | * @providesModule DraftEditorPlaceholder
67 | */
68 |
69 | .public-DraftEditorPlaceholder-root {
70 | color: #9197a3;
71 | position: absolute;
72 | z-index: 0;
73 | }
74 |
75 | .public-DraftEditorPlaceholder-hasFocus {
76 | color: #bdc1c9;
77 | }
78 |
79 | .DraftEditorPlaceholder-hidden {
80 | display: none;
81 | }
82 | /**
83 | * @providesModule DraftStyleDefault
84 | */
85 |
86 | .public-DraftStyleDefault-block {
87 | position: relative;
88 | white-space: pre-wrap;
89 | }
90 |
91 | /* @noflip */
92 |
93 | .public-DraftStyleDefault-ltr {
94 | direction: ltr;
95 | text-align: left;
96 | }
97 |
98 | /* @noflip */
99 |
100 | .public-DraftStyleDefault-rtl {
101 | direction: rtl;
102 | text-align: right;
103 | }
104 |
105 | /**
106 | * These rules provide appropriate text direction for counter pseudo-elements.
107 | */
108 |
109 | /* @noflip */
110 |
111 | .public-DraftStyleDefault-listLTR {
112 | direction: ltr;
113 | }
114 |
115 | /* @noflip */
116 |
117 | .public-DraftStyleDefault-listRTL {
118 | direction: rtl;
119 | }
120 |
121 | /**
122 | * Default spacing for list container elements. Override with CSS as needed.
123 | */
124 |
125 | .public-DraftStyleDefault-ul,
126 | .public-DraftStyleDefault-ol {
127 | margin: 16px 0;
128 | padding: 0;
129 | }
130 |
131 | /**
132 | * Default counters and styles are provided for five levels of nesting.
133 | * If you require nesting beyond that level, you should use your own CSS
134 | * classes to do so. If you care about handling RTL languages, the rules you
135 | * create should look a lot like these.
136 | */
137 |
138 | /* @noflip */
139 |
140 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR {
141 | margin-left: 1.5em;
142 | }
143 |
144 | /* @noflip */
145 |
146 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL {
147 | margin-right: 1.5em;
148 | }
149 |
150 | /* @noflip */
151 |
152 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR {
153 | margin-left: 3em;
154 | }
155 |
156 | /* @noflip */
157 |
158 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL {
159 | margin-right: 3em;
160 | }
161 |
162 | /* @noflip */
163 |
164 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR {
165 | margin-left: 4.5em;
166 | }
167 |
168 | /* @noflip */
169 |
170 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL {
171 | margin-right: 4.5em;
172 | }
173 |
174 | /* @noflip */
175 |
176 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR {
177 | margin-left: 6em;
178 | }
179 |
180 | /* @noflip */
181 |
182 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL {
183 | margin-right: 6em;
184 | }
185 |
186 | /* @noflip */
187 |
188 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR {
189 | margin-left: 7.5em;
190 | }
191 |
192 | /* @noflip */
193 |
194 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL {
195 | margin-right: 7.5em;
196 | }
197 |
198 | /**
199 | * Only use `square` list-style after the first two levels.
200 | */
201 |
202 | .public-DraftStyleDefault-unorderedListItem {
203 | list-style-type: square;
204 | position: relative;
205 | }
206 |
207 | .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0 {
208 | list-style-type: disc;
209 | }
210 |
211 | .public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1 {
212 | list-style-type: circle;
213 | }
214 |
215 | /**
216 | * Ordered list item counters are managed with CSS, since all list nesting is
217 | * purely visual.
218 | */
219 |
220 | .public-DraftStyleDefault-orderedListItem {
221 | list-style-type: none;
222 | position: relative;
223 | }
224 |
225 | /* @noflip */
226 |
227 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before {
228 | left: -36px;
229 | position: absolute;
230 | text-align: right;
231 | width: 30px;
232 | }
233 |
234 | /* @noflip */
235 |
236 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before {
237 | position: absolute;
238 | right: -36px;
239 | text-align: left;
240 | width: 30px;
241 | }
242 |
243 | /**
244 | * Counters are reset in JavaScript. If you need different counter styles,
245 | * override these rules. If you need more nesting, create your own rules to
246 | * do so.
247 | */
248 |
249 | .public-DraftStyleDefault-orderedListItem:before {
250 | content: counter(ol0) ". ";
251 | counter-increment: ol0;
252 | }
253 |
254 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before {
255 | content: counter(ol1) ". ";
256 | counter-increment: ol1;
257 | }
258 |
259 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before {
260 | content: counter(ol2) ". ";
261 | counter-increment: ol2;
262 | }
263 |
264 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before {
265 | content: counter(ol3) ". ";
266 | counter-increment: ol3;
267 | }
268 |
269 | .public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before {
270 | content: counter(ol4) ". ";
271 | counter-increment: ol4;
272 | }
273 |
274 | .public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset {
275 | counter-reset: ol0;
276 | }
277 |
278 | .public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset {
279 | counter-reset: ol1;
280 | }
281 |
282 | .public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset {
283 | counter-reset: ol2;
284 | }
285 |
286 | .public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset {
287 | counter-reset: ol3;
288 | }
289 |
290 | .public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset {
291 | counter-reset: ol4;
292 | }
293 |
294 | .RichEditor-root {
295 | background: #fff;
296 | border: 1px solid #ddd;
297 | font-family: 'Georgia', serif;
298 | font-size: 14px;
299 | padding: 15px;
300 | }
301 |
302 | .RichEditor-editor {
303 | border-top: 1px solid #ddd;
304 | cursor: text;
305 | font-size: 16px;
306 | margin-top: 10px;
307 | }
308 |
309 | .RichEditor-editor .public-DraftEditorPlaceholder-root,
310 | .RichEditor-editor .public-DraftEditor-content {
311 | margin: 0 -15px -15px;
312 | padding: 15px;
313 | }
314 |
315 | .RichEditor-editor .public-DraftEditor-content {
316 | min-height: 100px;
317 | }
318 |
319 | .RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root {
320 | display: none;
321 | }
322 |
323 | .RichEditor-editor .RichEditor-blockquote {
324 | border-left: 5px solid #eee;
325 | color: #666;
326 | font-family: 'Open Sans', 'Georgia', serif;
327 | font-style: italic;
328 | margin: 16px 0;
329 | padding: 10px 20px;
330 | }
331 |
332 | .RichEditor-editor .public-DraftStyleDefault-pre {
333 | background-color: rgba(0, 0, 0, 0.05);
334 | font-family: 'Open Sans', 'Georgia', serif;
335 | font-size: 16px;
336 | padding: 20px;
337 | }
338 |
339 | .RichEditor-controls {
340 | font-family: 'Open Sans', 'Georgia', serif;
341 | font-size: 14px;
342 | margin-bottom: 5px;
343 | user-select: none;
344 | }
345 |
346 | .RichEditor-styleButton {
347 | color: #999;
348 | cursor: pointer;
349 | margin-right: 16px;
350 | padding: 2px 0;
351 | display: inline-block;
352 | }
353 |
354 | .RichEditor-activeButton {
355 | color: #5890ff;
356 | }
357 |
358 |
359 |
--------------------------------------------------------------------------------
/app/stylesheets/style.scss:
--------------------------------------------------------------------------------
1 | @import "richText";
2 | @import "./variables";
3 | @import "./mixins";
4 | @import "./partials/margin";
5 | @import "./partials/text";
6 | @import "./partials/buttons";
7 | @import "./partials/header";
8 | @import "./partials/loading";
9 | @import "./pages/post_lists";
10 |
11 | @mixin placeholder($selector, $color) {
12 | #{$selector}::-webkit-input-placeholder {
13 | color: $color;
14 | transition: opacity 250ms ease-in-out;
15 | }
16 | #{$selector}::-moz-placeholder {
17 | color: $color;
18 | transition: opacity 250ms ease-in-out;
19 | }
20 | #{$selector}:-ms-input-placeholder {
21 | color: $color;
22 | transition: opacity 250ms ease-in-out;
23 | }
24 | #{$selector}:focus::-webkit-input-placeholder {
25 | color: lighten($color,10)
26 | }
27 | #{$selector}:focus::-moz-placeholder {
28 | color: lighten($color,10)
29 | }
30 | #{$selector}:focus:-ms-input-placeholder {
31 | color: lighten($color,10)
32 | }
33 | }
34 |
35 | body {
36 | font-family: 'Open Sans', sans-serif;
37 | background: #fafafa;
38 | font-size: 14px;
39 | }
40 |
41 | .bg-white {
42 | background: #ffffff;
43 | padding: 30px 30px;
44 | position: relative;
45 | //min-height: 90%;
46 | h1.title{
47 | color: #595959;
48 | font-weight: 400;
49 | font-family: 'Raleway';
50 | font-size: 20px;
51 | margin-top: 0px;
52 | margin-bottom: 20px;
53 | }
54 | }
55 |
56 | .form-control {
57 | border: 1px solid #ddd;
58 | border-radius: 0px;
59 | box-shadow: none;
60 | font-size: 14px;
61 | &:focus {
62 | box-shadow: none;
63 | }
64 | }
65 | //@include placeholder('input', #595959);
66 |
67 | i[class*="icon-"]{
68 | top: 1px;
69 | position: relative;
70 | }
71 |
72 | .form {
73 | .control-label {
74 | font-weight: 600;
75 | font-size: 14px;
76 | color: #727272;
77 | margin-bottom: 10px;
78 | font-weight: 400;
79 | }
80 | .has-error{
81 | .form-control{
82 | border-color: $bg-primary-color;
83 | &:focus{
84 | border-color: $bg-primary-color;
85 | }
86 | }
87 | }
88 | .form-control {
89 | &:focus{
90 | box-shadow: none;
91 | border: 1px solid #dddddd;
92 | }
93 | }
94 | @include placeholder('.form-control', #c7c7c7);
95 |
96 | }
97 |
98 | .loading {
99 | box-sizing: border-box;
100 | width: 48px;
101 | height: 48px;
102 | margin: 20px auto 0 auto;
103 |
104 | -webkit-animation: spinner 1s ease infinite;
105 | -moz-animation: spinner 1s ease infinite;
106 | animation: spinner 1s ease infinite;
107 |
108 | border: 8px solid rgba(196, 196, 196, .75);
109 | border-right-color: transparent;
110 | border-radius: 50%;
111 | }
112 |
113 | @keyframes spinner {
114 | from {
115 | transform: rotate(0deg);
116 | }
117 | to {
118 | transform: rotate(360deg)
119 | }
120 | }
121 |
122 | @-webkit-keyframes spinner {
123 | from {
124 | transform: rotate(0deg);
125 | }
126 | to {
127 | transform: rotate(360deg);
128 | }
129 | }
130 |
131 | @-moz-keyframes spinner {
132 | from {
133 | transform: rotate(0deg);
134 | }
135 | to {
136 | transform: rotate(360deg);
137 | }
138 | }
--------------------------------------------------------------------------------
/app/test/Test.js:
--------------------------------------------------------------------------------
1 | import Auth from '../api/auth/index';
2 | import 'whatwg-fetch';
3 | import firebase from '../api/Firebase';
4 |
5 | localStorage.setItem('hello', {a: 'tung', b: 'tien'});
6 |
7 | let myHeaders = new Headers();
8 | myHeaders.set('Access-Control-Allow-Origin', '*');
9 |
10 |
11 |
12 | //userLogin('tungptkh@gmail.com','123456').then(function(user){
13 | // console.log(user);
14 | //}).catch(function(err){
15 | // console.log(err);
16 | //});
17 |
18 | //var auth = new Auth();
19 |
20 | //auth.logined().then(function (user) {
21 | // let users = {
22 | // username: 'tung',
23 | // first_name: 'hello'
24 | // }
25 | // auth.updateProfile(users).then(function(){
26 | // console.log('updated');
27 | // })
28 | //
29 | //});
30 |
31 | /*auth.register('tungpt@gmail.com', '123456', {}).then((user)=> {
32 | console.log(user);
33 | }).catch((err) => {
34 | console.log(err);
35 | });*/
--------------------------------------------------------------------------------
/app/utils/firebaseUtils.js:
--------------------------------------------------------------------------------
1 | export function mergeArrayObjectWithKey(objects) {
2 | let new_objects = [];
3 | for (let key in objects) {
4 | let object = Object.assign(objects[key], {id: key});
5 | new_objects.push(object);
6 | }
7 | return new_objects;
8 | }
9 |
10 | export function mergeObjectWithKey(object, key) {
11 | return Object.assign(object, {id: key});
12 | }
--------------------------------------------------------------------------------
/app/utils/index.js:
--------------------------------------------------------------------------------
1 | import CompareUID from '../redux/containers/CompareUID';
2 | import * as firebaseUtils from './firebaseUtils';
3 | import * as reduxAwait from './reduxAwait';
4 |
5 | export function compareUID(compare_id, Component){
6 | return
7 | }
8 |
9 | export function setTitle(title){
10 | document.title = title;
11 | }
12 |
13 | export {
14 | reduxAwait
15 | }
--------------------------------------------------------------------------------
/app/utils/reduxAwait.js:
--------------------------------------------------------------------------------
1 | import {connect as reduxConnect} from 'react-redux';
2 |
3 | export function connect(mapStateToProps, ...args) {
4 | return reduxConnect((state, ownProps) => {
5 | const props = mapStateToProps(state, ownProps);
6 | let awaitStatuses = (state.await.statuses instanceof Array) ? {} : state.await.statuses;
7 | let awaitErrors = (state.await.errors instanceof Array) ? {} : state.await.errors;
8 | return {...props, awaitStatuses, awaitErrors};
9 | }, ...args)
10 | }
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | webpack_dev: {
3 | domain: 'localhost', //You can change to: 0.0.0.0, 123.4.5.6, and other ..
4 | port: '8080'
5 | }
6 | }
--------------------------------------------------------------------------------
/configs/webpack_dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | domain: '0.0.0.0',
3 | port: '8080'
4 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firebase": "react-redux-blog",
3 | "public": "public",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ]
9 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-starter",
3 | "version": "1.0.0",
4 | "description": "starter",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node_modules/.bin/webpack-dev-server --progress",
9 | "build": "NODE_ENV=production node_modules/.bin/webpack -p --progress --colors"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/thanhtungdp/react-starter.git"
14 | },
15 | "author": "DavidEvans",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/thanhtungdp/react-starter/issues"
19 | },
20 | "homepage": "https://github.com/thanhtungdp/react-starter#readme",
21 | "dependencies": {
22 | "animate": "0.0.5",
23 | "animate.css": "^3.5.1",
24 | "axios": "^0.11.0",
25 | "babel-polyfill": "^6.7.4",
26 | "bootstrap-sass": "^3.3.6",
27 | "classnames": "^2.2.5",
28 | "draft-js": "^0.6.0",
29 | "draft-js-export-html": "^0.1.13",
30 | "fbemitter": "^2.0.2",
31 | "file-loader": "^0.8.5",
32 | "firebase": "^2.4.2",
33 | "flux": "^2.1.1",
34 | "font-awesome": "^4.5.0",
35 | "icheck": "^1.0.2",
36 | "jquery": "^2.2.3",
37 | "react": "^15.0.2",
38 | "react-addons-css-transition-group": "^0.14.8",
39 | "react-addons-transition-group": "^15.0.2",
40 | "react-addons-update": "^0.14.8",
41 | "react-bootstrap": "^0.29.1",
42 | "react-dom": "^0.14.8",
43 | "react-icheck": "^0.2.2",
44 | "react-masonry-component": "^4.0.4",
45 | "react-redux": "^4.4.1",
46 | "react-router": "^2.3.0",
47 | "react-router-redux": "^4.0.2",
48 | "redux": "^3.3.1",
49 | "redux-async": "^2.0.1",
50 | "redux-await": "^5.0.1",
51 | "redux-create-reducer": "^1.1.0",
52 | "redux-form": "^5.2.2",
53 | "redux-logger": "^2.6.1",
54 | "redux-promise": "^0.5.3",
55 | "redux-saga": "^0.9.5",
56 | "redux-thunk": "^2.0.1",
57 | "request": "^2.72.0",
58 | "simple-line-icons": "^2.3.0",
59 | "url-loader": "^0.5.7",
60 | "validator": "^5.2.0",
61 | "whatwg-fetch": "^0.11.0"
62 | },
63 | "devDependencies": {
64 | "babel-core": "^6.7.2",
65 | "babel-loader": "^6.2.4",
66 | "babel-plugin-react-transform": "^2.0.2",
67 | "babel-preset-es2015": "^6.6.0",
68 | "babel-preset-react": "^6.5.0",
69 | "babel-preset-stage-0": "^6.5.0",
70 | "css-loader": "^0.23.1",
71 | "node-sass": "^3.4.2",
72 | "react-hot-loader": "^1.3.0",
73 | "react-transform-hmr": "^1.0.4",
74 | "sass-loader": "^3.2.0",
75 | "style-loader": "^0.13.1",
76 | "webpack": "^1.12.14",
77 | "webpack-dev-server": "^1.14.1"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/public/30ba5cd4a3ad1d61e85a4847eb174dd2.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/30ba5cd4a3ad1d61e85a4847eb174dd2.woff2
--------------------------------------------------------------------------------
/public/404a525502f8e5ba7e93b9f02d9e83a9.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/404a525502f8e5ba7e93b9f02d9e83a9.eot
--------------------------------------------------------------------------------
/public/448c34a56d699c29117adc64c43affeb.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/448c34a56d699c29117adc64c43affeb.woff2
--------------------------------------------------------------------------------
/public/4658299167a2c591b4abac2f9a2a1476.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/4658299167a2c591b4abac2f9a2a1476.ttf
--------------------------------------------------------------------------------
/public/891e3f340c1126b4c7c142e5f6e86816.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/891e3f340c1126b4c7c142e5f6e86816.woff
--------------------------------------------------------------------------------
/public/926c93d201fe51c8f351e858468980c3.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/926c93d201fe51c8f351e858468980c3.woff2
--------------------------------------------------------------------------------
/public/b0a05c8de588b1af413ed5b07d355803.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/b0a05c8de588b1af413ed5b07d355803.woff
--------------------------------------------------------------------------------
/public/c2cf0100e8770e3d4019a2edaa39adaa.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/c2cf0100e8770e3d4019a2edaa39adaa.eot
--------------------------------------------------------------------------------
/public/e18bbf611f2a2e43afc071aa2f4e1512.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/e18bbf611f2a2e43afc071aa2f4e1512.ttf
--------------------------------------------------------------------------------
/public/f4769f9bdb7466be65088239c12046d1.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/f4769f9bdb7466be65088239c12046d1.eot
--------------------------------------------------------------------------------
/public/fa2772327f55d8198301fdb8bcfc8158.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/fa2772327f55d8198301fdb8bcfc8158.woff
--------------------------------------------------------------------------------
/public/fb650aaf10736ffb9c4173079616bf01.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/fb650aaf10736ffb9c4173079616bf01.ttf
--------------------------------------------------------------------------------
/public/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firebase": "vndev-chat",
3 | "public": "/",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ]
9 | }
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanhtungdp/react-redux-firebase-blog/08648f8638754fef0bf2e8efedc1dbbea9b5acb4/public/images/logo.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React + Redux + Firebase blog example by vndev.co
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 | var config_webpath_dev = require('./configs/webpack_dev');
4 |
5 | var config = {
6 | devtool: 'eval',
7 | entry: __dirname + '/app/App.js',
8 | resolve: {root: [__dirname + "/sass"]},
9 | output: {
10 | path: __dirname + '/public',
11 | filename: "bundle.js"
12 | },
13 | plugins: [
14 | new webpack.HotModuleReplacementPlugin()
15 | ],
16 | module: {
17 | loaders: [
18 | {
19 | test: /\.jsx?$/,
20 | loader: 'babel',
21 | exclude: /(node_modules|bower_components)/
22 | },
23 | {
24 | test: /\.css$/,
25 | loader: 'style-loader!css-loader'
26 | },
27 | {
28 | test: /\.scss$/,
29 | loaders: ["style", "css", "sass"]
30 | },
31 | {test: /\.(png|jpg)$/, loader: 'file-loader'},
32 | {test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"},
33 | {test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"},
34 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream"},
35 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file"},
36 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml"}
37 | ]
38 | },
39 | devServer: {
40 | contentBase: "./public",
41 | colors: true,
42 | historyApiFallback: true,
43 | inline: true,
44 | hot: true,
45 | host: config_webpath_dev.domain,
46 | port: config_webpath_dev.port
47 | },
48 | }
49 |
50 | /*
51 | * If bundling for production, optimize output
52 | */
53 | if (process.env.NODE_ENV === 'production') {
54 | config.devtool = false;
55 | config.plugins = [
56 | new webpack.optimize.OccurenceOrderPlugin(),
57 | new webpack.optimize.UglifyJsPlugin({comments: false}),
58 | new webpack.DefinePlugin({
59 | 'process.env': {NODE_ENV: JSON.stringify('production')}
60 | })
61 | ];
62 | }
63 | ;
64 |
65 | module.exports = config;
--------------------------------------------------------------------------------