├── .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 |
132 |
133 | 143 |
144 |
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