Working on the editor UI. Needs some refactoring components and wire up api middleware.
3
TODO.txt
|
@ -9,13 +9,14 @@
|
|||
|
||||
Short List:
|
||||
- Some Testing
|
||||
- Font Awesome / Better Glyphicons
|
||||
- Env Configs
|
||||
- Collapsable Button Lists
|
||||
- Draft.JS
|
||||
- DRY up Sass Modules
|
||||
- Async API Middleware
|
||||
- CombineReducers in App.js
|
||||
- Import Actions into Reducers
|
||||
- Import Action consts into Reducers
|
||||
- PropTypes
|
||||
|
||||
- Loading Indicators
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"clean-webpack-plugin": "^0.1.17",
|
||||
"css-loader": "^0.28.7",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"manifest-revision-webpack-plugin": "^0.4.1",
|
||||
"node-sass": "^4.6.0",
|
||||
"postcss-loader": "^2.0.8",
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
const setCurrentChapter = id =>
|
||||
({
|
||||
type: 'GET_TEXT_DATA',
|
||||
id: id
|
||||
})
|
||||
|
||||
const toggleHighlight = () =>
|
||||
({
|
||||
type: 'TOGGLE_HIGHLIGHT'
|
||||
})
|
||||
|
||||
const updateEditorState = editorState =>
|
||||
({
|
||||
type: 'UPDATE_EDITOR_STATE',
|
||||
payload: editorState
|
||||
})
|
||||
|
||||
export {
|
||||
setCurrentChapter,
|
||||
toggleHighlight,
|
||||
updateEditorState
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import axios from 'axios'
|
||||
import EditorState from 'draft-js'
|
||||
|
||||
let apiRoute = '/api/chapters/'
|
||||
|
||||
const SELECT_CHAPTER = 'SELECT_CHAPTER'
|
||||
const GET_CHAPTER_DATA = 'GET_CHAPTER_DATA'
|
||||
const GET_CHAPTER_DATA_RECEIVED = 'GET_CHAPTER_DATA_RECEIVED'
|
||||
const GET_CHAPTER_DATA_ERROR = 'GET_CHAPTER_DATA_ERROR'
|
||||
const GET_TEXT_DATA = 'GET_TEXT_DATA'
|
||||
const GET_TEXT_DATA_RECEIVED = 'GET_TEXT_DATA_RECEIVED'
|
||||
const GET_TEXT_DATA_ERROR = 'GET_TEXT_DATA_ERROR'
|
||||
const UPDATE_EDITOR_STATE = 'UPDATE_EDITOR_STATE'
|
||||
const TOGGLE_HIGHLIGHT = 'TOGGLE_HIGHLIGHT'
|
||||
const UPDATE_CHAPTER_TITLE = 'UPDATE_CHAPTER_TITLE'
|
||||
|
||||
export const setCurrentChapter = id =>
|
||||
({
|
||||
type: GET_TEXT_DATA,
|
||||
id: id
|
||||
})
|
||||
|
||||
export const toggleHighlight = () =>
|
||||
({
|
||||
type: TOGGLE_HIGHLIGHT
|
||||
})
|
||||
|
||||
export const updateChapterTitleInput = chapterTitleInput => {
|
||||
return ({
|
||||
type: UPDATE_CHAPTER_TITLE,
|
||||
data: chapterTitleInput.target.value
|
||||
})
|
||||
}
|
||||
|
||||
export const updateEditorState = editorState =>
|
||||
({
|
||||
type: UPDATE_EDITOR_STATE,
|
||||
data: editorState
|
||||
})
|
||||
|
||||
export const chapterDataReceived = data =>
|
||||
({
|
||||
type: GET_CHAPTER_DATA_RECEIVED,
|
||||
data
|
||||
})
|
||||
|
||||
export const chapterDataError = error =>
|
||||
({
|
||||
type: GET_CHAPTER_DATA_ERROR,
|
||||
error
|
||||
})
|
||||
|
||||
export const textDataReceived = data =>
|
||||
({
|
||||
type: GET_TEXT_DATA_RECEIVED,
|
||||
data
|
||||
})
|
||||
|
||||
export const textDataError = error =>
|
||||
({
|
||||
type: GET_TEXT_DATA_ERROR,
|
||||
error
|
||||
})
|
||||
|
||||
export const setChapterEditor = chapter =>
|
||||
({
|
||||
type: SET_EDITED_CHAPTER,
|
||||
chapter
|
||||
})
|
||||
|
||||
export const logger = store => next => action => {
|
||||
console.group(action.type)
|
||||
console.info('dispatching', action)
|
||||
let result = next(action)
|
||||
console.log('next state', store.getState())
|
||||
console.log('chapter count', store.getState().chapters.length)
|
||||
console.groupEnd(action.type)
|
||||
return result
|
||||
}
|
||||
|
||||
export const joyceAPIService = store => next => action => {
|
||||
next(action)
|
||||
switch(action.type) {
|
||||
case GET_CHAPTER_DATA:
|
||||
axios.get(apiRoute).then(res => {
|
||||
const data = res.data
|
||||
return next(chapterDataReceived(data))
|
||||
}).catch(error => {
|
||||
return next(chapterDataError(error))
|
||||
})
|
||||
break
|
||||
case GET_TEXT_DATA:
|
||||
axios.get(apiRoute + action.id).then(res=> {
|
||||
const data = res.data
|
||||
return next(textDataReceived(data))
|
||||
}).catch(error => {
|
||||
return next(textDataError(error))
|
||||
})
|
||||
break
|
||||
case CREATE_CHAPTER:
|
||||
const nextNumber = store.getState().chapters.length + 1
|
||||
const chapter = {
|
||||
number: nextNumber,
|
||||
title: '',
|
||||
text: EditorState.createEmpty(),
|
||||
}
|
||||
store.dispatch(setChapterEditor(chapter))
|
||||
break
|
||||
defaut:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export const foo = 'bar'
|
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,30 @@
|
|||
const glyphiconItalic = require('./glyphicons-102-italic.png')
|
||||
const glyphiconBold = require('./glyphicons-103-bold.png')
|
||||
const glyphiconUnderline = require('./glyphicons-104-text-underline.png')
|
||||
|
||||
const glyphiconAlignLeft = require('./glyphicons-111-align-left.png')
|
||||
const glyphiconAlignCenter = require('./glyphicons-112-align-center.png')
|
||||
const glyphiconAlignRight = require('./glyphicons-113-align-right.png')
|
||||
|
||||
const glyphiconChevronLeft = require('./glyphicons-225-chevron-left.png')
|
||||
const glyphiconChevronRight = require('./glyphicons-224-chevron-right.png')
|
||||
|
||||
const glyphiconPlusSign = require('./glyphicons-191-plus-sign.png')
|
||||
const glyphiconMinusSign = require('./glyphicons-192-minus-sign.png')
|
||||
const glyphiconRemoveSign = require('./glyphicons-193-remove-sign.png')
|
||||
const glyphiconOKSign = require('./glyphicons-194-ok-sign.png')
|
||||
|
||||
export {
|
||||
glyphiconItalic,
|
||||
glyphiconBold,
|
||||
glyphiconUnderline,
|
||||
glyphiconAlignLeft,
|
||||
glyphiconAlignCenter,
|
||||
glyphiconAlignRight,
|
||||
glyphiconChevronLeft,
|
||||
glyphiconChevronRight,
|
||||
glyphiconPlusSign,
|
||||
glyphiconMinusSign,
|
||||
glyphiconRemoveSign,
|
||||
glyphiconOKSign
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
const EditorButtonGroup = () =>
|
||||
<div className='editor_button_group'>
|
||||
<div className='btn-group' role='group'>
|
||||
<button type='button' className='btn btn-secondary'>Bold</span></button>
|
||||
<button type='button' className='btn btn-secondary'>Italics</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
export default EditorButtonGroup
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import ChapterListContainer from '../containers/chapterListContainer'
|
||||
import NewChapterButtonContainer from '../containers/newChapterButtonContainer'
|
||||
import SidebarSpacer from './sidebarSpacer'
|
||||
|
||||
const EditorSidebar = () =>
|
||||
<div className="col-md-3" id="sidebar">
|
||||
<SidebarSpacer />
|
||||
<NewChapterButtonContainer />
|
||||
<SidebarSpacer />
|
||||
<ChapterListContainer />
|
||||
</div>
|
||||
|
||||
export default EditorSidebar
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
import { glyphiconPlusSign } from '../assets'
|
||||
|
||||
const NewChapterButton = () =>
|
||||
<div>
|
||||
<div id="highlight_button" className="text-center">
|
||||
<button className='btn btn-outline-success btn-lg'>
|
||||
New Chapter <img src={glyphiconPlusSign} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
export default NewChapterButton
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { setCurrentChapter } from '../actions/actions'
|
||||
import { setCurrentChapter } from '../actions'
|
||||
|
||||
import ChapterList from '../components/chapterList'
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
|
||||
import TextEditorContainer from '../containers/textEditorContainer'
|
||||
import Navbar from '../components/navbar'
|
||||
import Sidebar from '../components/sidebar'
|
||||
import EditorSidebar from '../components/editorSidebar'
|
||||
import Content from '../components/content'
|
||||
|
||||
const EditorContainer = () =>
|
||||
|
@ -10,7 +10,7 @@ const EditorContainer = () =>
|
|||
<Navbar />
|
||||
<div id='joyce_editor' className='container-fluid'>
|
||||
<div className="row">
|
||||
<Sidebar />
|
||||
<EditorSidebar />
|
||||
<Content>
|
||||
<TextEditorContainer />
|
||||
</Content>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { toggleHighlight } from '../actions/actions'
|
||||
import { toggleHighlight } from '../actions'
|
||||
|
||||
import HighlightButton from '../components/highlightButton'
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { createNewChapter } from '../actions'
|
||||
|
||||
import NewChapterButton from '../components/newChapterButton'
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onNewChapterClick: () => {
|
||||
dispatch(createNewChapter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NewChapterButtonContainer = connect(mapDispatchToProps)(NewChapterButton)
|
||||
|
||||
export default NewChapterButtonContainer
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { setCurrentChapter } from '../actions/actions'
|
||||
import { setCurrentChapter } from '../actions'
|
||||
|
||||
class Page extends React.Component {
|
||||
render() {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { setCurrentChapter } from '../actions/actions'
|
||||
import { setCurrentChapter } from '../actions'
|
||||
|
||||
import PageContainer from '../containers/pageContainer'
|
||||
import Content from '../components/content'
|
||||
|
|
|
@ -1,21 +1,57 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { updateEditorState } from '../actions/actions'
|
||||
import { updateEditorState, updateChapterTitleInput } from '../actions'
|
||||
|
||||
import { Editor, convertToRaw } from 'draft-js';
|
||||
|
||||
import EditorButtonGroup from '../components/editorButtonGroup'
|
||||
import { glyphiconBold, glyphiconItalic, glyphiconUnderline, glyphiconAlignLeft, glyphiconAlignCenter, glyphiconAlignRight } from '../assets'
|
||||
|
||||
const TextEditor = ({editorState, onSaveEditorState}) =>
|
||||
<div id="text_editor">
|
||||
<EditorButtonGroup />
|
||||
const TextEditor = ({editorState, onSaveEditorState, chapterTitleInput, onChapterTitleChange}) =>
|
||||
<div id='editor_container'>
|
||||
<div id='editor_topbar'>
|
||||
<div className='row'>
|
||||
<div className='col-md-4'>
|
||||
<h4>Chapter 1:</h4>
|
||||
</div>
|
||||
<div className='col-md-8'>
|
||||
<input type="text" value={chapterTitleInput} onChange={onChapterTitleChange}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='btn-group col-md-3 offset-md-6' role='group'>
|
||||
<button type='button' className='btn btn-info btn-sm'><img src={glyphiconBold} /></button>
|
||||
<button type='button' className='btn btn-info btn-sm'><img src={glyphiconItalic} /></button>
|
||||
<button type='button' className='btn btn-info btn-sm'><img src={glyphiconUnderline} /></button>
|
||||
</div>
|
||||
<div className='btn-group col-md-3' role='group'>
|
||||
<button type='button' className='btn btn-info btn-sm'><img src={glyphiconAlignLeft} /></button>
|
||||
<button type='button' className='btn btn-info btn-sm'><img src={glyphiconAlignCenter} /></button>
|
||||
<button type='button' className='btn btn-info btn-sm'><img src={glyphiconAlignRight} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='editor_content'>
|
||||
<Editor editorState={editorState} onChange={onSaveEditorState} />
|
||||
{JSON.stringify(editorState.getSelection())}
|
||||
</div>
|
||||
|
||||
<div id='editor_bottombar'>
|
||||
<div className='row'>
|
||||
<div className='col-md-5'>
|
||||
<button id='editor_delete' type='button' className='btn btn-danger btn-sm'>Delete</button>
|
||||
</div>
|
||||
<div className='col-md-5 offset-md-2'>
|
||||
<button id='editor_save' type='button' className='btn btn-success btn-sm'>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
editorState: state.editorState
|
||||
editorState: state.editorState,
|
||||
chapterTitleInput: state.chapterTitleInput
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,6 +59,9 @@ const mapDispatchToProps = dispatch => {
|
|||
return {
|
||||
onSaveEditorState: editorState => {
|
||||
dispatch(updateEditorState(editorState))
|
||||
},
|
||||
onChapterTitleChange: chapterTitleInput => {
|
||||
dispatch(updateChapterTitleInput(chapterTitleInput))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,19 @@ import { createStore, combineReducers, applyMiddleware } from 'redux'
|
|||
import { Provider } from 'react-redux'
|
||||
import 'bootstrap'
|
||||
|
||||
import { chapters, highlightActive, currentChapter, editorState } from './reducers/editor'
|
||||
import joyceAPIService from './middleware'
|
||||
import { chapters, highlightActive, currentChapter, chapterTitleInput, editorState } from './reducers/editor'
|
||||
import { joyceAPIService, logger } from './actions'
|
||||
import EditorContainer from './containers/editorContainer'
|
||||
|
||||
const reduceEditor = combineReducers({
|
||||
chapters,
|
||||
currentChapter,
|
||||
chapterTitleInput,
|
||||
highlightActive,
|
||||
editorState
|
||||
})
|
||||
|
||||
let store = createStore(reduceEditor, applyMiddleware(joyceAPIService))
|
||||
let store = createStore(reduceEditor, applyMiddleware(joyceAPIService, logger))
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
|
@ -23,3 +24,6 @@ ReactDOM.render(
|
|||
</Provider>,
|
||||
document.getElementById('wrapper')
|
||||
)
|
||||
|
||||
store.dispatch({type: 'GET_CHAPTER_DATA'})
|
||||
store.dispatch({type: 'GET_TEXT_DATA', id: 1})
|
|
@ -5,7 +5,7 @@ import { Provider } from 'react-redux'
|
|||
import 'bootstrap'
|
||||
|
||||
import { chapters, currentChapter, highlightActive } from './reducers/reader'
|
||||
import joyceAPIService from './middleware'
|
||||
import { joyceAPIService } from './actions'
|
||||
import { ReaderContainer } from './containers/readerContainer'
|
||||
|
||||
const reduceReader = combineReducers({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { EditorState } from 'draft-js'
|
||||
import objectAssign from 'object-assign' // Object.assign() polyfill for older browsers
|
||||
import actions from '../actions/actions'
|
||||
import actions from '../actions'
|
||||
|
||||
const chapters = (state=[], action) => {
|
||||
switch(action.type) {
|
||||
|
@ -32,8 +32,16 @@ const currentChapter = (state={}, action) => {
|
|||
const editorState = (state=(EditorState.createEmpty()), action) => {
|
||||
switch(action.type) {
|
||||
case 'UPDATE_EDITOR_STATE':
|
||||
console.log(action.payload.getSelection())
|
||||
return action.payload
|
||||
return action.data
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const chapterTitleInput = (state='', action) => {
|
||||
switch(action.type) {
|
||||
case 'UPDATE_CHAPTER_TITLE':
|
||||
return action.data
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
@ -43,5 +51,6 @@ export {
|
|||
chapters,
|
||||
highlightActive,
|
||||
currentChapter,
|
||||
chapterTitleInput,
|
||||
editorState
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
$theme-colors: (
|
||||
"info": #537577,
|
||||
"danger": #72030a,
|
||||
"success": #118734,
|
||||
"dark": #07383a,
|
||||
"primary": #824500
|
||||
);
|
|
@ -108,13 +108,50 @@ nav a:hover {
|
|||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#text_editor {
|
||||
#editor_topbar {
|
||||
margin: 0.8% 0;
|
||||
}
|
||||
|
||||
#editor_topbar input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#editor_topbar button img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#editor_content {
|
||||
height: 100%;
|
||||
background-color: rgba(256,256,256,.8);
|
||||
box-shadow: 2px 5px 10px 1px rgba(0, 0, 0, 0.3);
|
||||
padding: 3% 8%;
|
||||
margin: 0.8% 0;
|
||||
border: 1px solid $border_color;
|
||||
border-radius: 5px;
|
||||
padding: 3% 8%;
|
||||
}
|
||||
|
||||
#editor_content div {
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#editor_topbar button img {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
#editor_bottombar {
|
||||
margin: 0.8% 0;
|
||||
}
|
||||
|
||||
#editor_delete {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#editor_save {
|
||||
float: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#page {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin')
|
||||
const ManifestRevisionPlugin = require('manifest-revision-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const ManifestRevisionPlugin = require('manifest-revision-webpack-plugin')
|
||||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
|
||||
const rootAssetPath = './src/';
|
||||
const rootAssetPath = './src/'
|
||||
|
||||
let pathsToClean = [
|
||||
'static/'
|
||||
|
@ -25,7 +25,7 @@ module.exports = {
|
|||
},
|
||||
output: {
|
||||
publicPath: "/static/js/",
|
||||
filename: '[name].[chunkhash].js',
|
||||
filename: '[name].[hash].js',
|
||||
path: path.resolve(__dirname, 'static/js/')
|
||||
},
|
||||
watch: true,
|
||||
|
@ -44,6 +44,12 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(png)$/,
|
||||
use: {
|
||||
loader: 'file-loader'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(scss)$/,
|
||||
use: [{
|
||||
|
@ -75,6 +81,6 @@ module.exports = {
|
|||
'window.jQuery': 'jquery',
|
||||
Popper: ['popper.js', 'default']
|
||||
}),
|
||||
new CleanWebpackPlugin(pathsToClean)
|
||||
new CleanWebpackPlugin(pathsToClean),
|
||||
],
|
||||
};
|