We will build app for video streaming. Install it with npm install --save react-router-dom
.
We will use Semantic UI
library for css - www.semantic-ui.com
.
Structure
streams
|
|--- api
|
|
|--- client
|
|
--- src
|
--- history
| |
| -- history . js
|
|
-- actions
| |
| -- index . js
|
-- reducers
| |
| -- index . js
|
|-- components
|
-- App . js
-- Header . js
-- GoogleAuth . js
-- Modal . js
|
-- streams
|
-- StreamForm . js
-- StreamList . js
-- StreamCreate . js
-- StreamEdit . js
-- StreamDelete . js
-- StreamShow . js
JSON API SERVER
First we will do API server, which will use REST-ful conventions (standardized system for designing APIs). We will be then able to do requests to this server to create new stream, etc …
So REST conventions is refering to standardized system of routes and request methods used to commit or operate different actions.
We will use https://npmjs.com/package/json-server
for this.
Under api
directory we will run following commands:
npm init
npm install -- save json - server
In docuementation of JSON server
we can find, that objects will be stored in db.json
file. So in our case, we will store streams
.
api/db.json
{
" streams " : []
}
api/package.json
{
...
" scripts " : {
" start " : " json-server -p 3001 -w db.json " //p is port, w means it is watching db.json for any changes
}
...
}
In api
dir run npm start
, that will start JSON server
. We can use it now to manipulate our streams following REST convetions.
REACT APP
client/index.html
After we put this inside index.html
we should be able to write gapi
into console.
< script src = " https://apis.google.com/js/api.js " >< /script>
client/src/index.js
import React from ' react ' ;
import ReactDOM from ' react-dom ' ;
import { Provider } fomr ' react-redux ' ;
import { createStore , applyMiddleware , compose } from ' redux ' ;
import reduxThunk from ' redux-thunk ' ;
import App from ' ./components/App ' ;
import reducers from ' ./reducers ' ;
cosnt composeEnhancers = window . __REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose ;
const store = createStore (
reducers ,
composeEnhancers ( applyMiddleware ( reduxThunk ))
);
ReactDOM . render (
< Provider store = { store } >
< App />
< /Provider> ,
document . querySelector ( ' #root ' )
)
Apis
client/src/apis/streams.js
import axios from ' axios ' ;
export default axios . create ({
baseURL : ' http://localhost:3001 '
});
Components
client/src/components/Header.js
import React from ' react ' ;
import { Link } from ' react-router-dom ' ;
import GoogleAuth from ' ./GoogleAuth ' ;
const Header = () => {
return (
< div class = " ui secondary poiting menu " >
< Link to = " / " >
Streams
< /Link >
< div class = " right menu " >
< Link to = " / " className = " item " >
All Streams
< /Link >
< GoogleAuth />
< /div >
< /div >
);
}
export default Header ;
client/src/components/GoogleAuth.js
import React from ' react ' ;
import { connect } from ' react-redux ' ;
import { signIn , singOut } from ' ../actions ' ;
class GoogleAuth extends React . Component {
componentDidMount () {
window . gapi . load ( ' client:auth2 ' , () => {
window . gapi . client . init ({
clientId : OUR_CLIENT_ID ,
scope : ' email '
}). then (() => {
this . auth = window . gapi . auth2 . getAuthInstance ();
this . onAuthChange ( this . auth . isSignedIn . get ());
this . auth . isSignedIn . listen ( this . onAuthChange );
});
});
}
onAuthChange = ( isSignedIn ) => {
if ( isSignedIn ) {
this . props . signIn ( this . auth . currentUser . get (). getId ());
} else {
this . props . signOut ();
}
};
onSignInClick = () => {
this . auth . signIn ();
}
onSignOutClick = () => {
this . auth . signOut ();
}
renderAuthButton () {
if ( this . props . isSignedIn === null ) {
return null ;
} else if ( this . props . isSignedIn ) {
return (
< button onclick = { this . onSignOutClick } className = " ui red google button " >
< i className = " google icon " />
Sign out
< /button >
);
} else {
return (
< button onclick = { this . onSignInClick } className = " ui red google button " >
< i className = " google icon " />
Sign in with Google
< /button >
);
}
}
render () {
return < div > { this . renderAuthButton ()} < /div> ;
}
}
const mapStateToProps = ( st ate ) => {
return { isSignedIn : state . auth . isSignedIn }
};
export default connect ( mapStateToProps , { signIn , signOut })( GoogleAuth );
client/src/components/Modal.js
modal popup window
import React from ' react ' ;
import ReactDOM from ' react-dom ' ;
import history from ' ../history ' ;
const Modal = props => {
return ReactDOM . createPortal (
< div
onclick = { props . onDismiss }
className = " ui dimmer modals visible active "
>
< div
onClick = {( e ) => e . stopPropagation ()}
className = " ui standard modal visible active "
>
< div className = " header " > { props . title } < /div >
< div className = " content " >
{ props . content }
< /div >
< div className = " actions " >
{ props . actions }
< /div >
< /div >
< /div> ,
document . querySelector ( ' #modal ' )
);
};
export default Modal ;
client/src/components/App.js
import React from ' react ' ;
import { Router , Route , Switch } from ' react-router-dom ' ;
import StreamCreate from ' ./streams/StreamCreate ' ;
import StreamEdit from ' ./streams/StreamEdit ' ;
import StreamDelete from ' ./streams/StreamDelete ' ;
import StreamList from ' ./streams/StreamList ' ;
import StreamShow from ' ./streams/StreamShow ' ;
import Header from ' ./Header ' ;
import history from ' ../history ' ;
const App = () => {
return (
< div className = " ui container " >
< Router history = { history } >
< div >
< Header />
/*
we are using switch, without it streams/3 will show both ScreamCreate and StreamShow
*/
< Switch >
< Route path = " / " exact component = { StreamList } / >
< Route path = " /streams/new " exact component = { StreamCreate } / >
< Route path = " /streams/edit/:id " exact component = { StreamEdit } / >
< Route path = " /streams/delete/:id " exact component = { StreamDelete } / >
< Route path = " /streams/:id " exact component = { StreamShow } / >
< /Switch >
< /div >
< /Router >
< /div >
);
};
export default App ;
client/src/components/streams/StreamList.js
import React from ' react ' ;
import { connect } from ' react-redux ' ;
import { Link } from ' react-router-dom ' ;
import { fetchStreams } from ' ../../actions ' ;
class StreamList extensd React . Component {
componentDidMount () {
this . props . fetchStreams ();
}
renderAdmin ( stream ) {
if ( stream . userId === this . props . currentUserId ) {
return (
< div className = " right floated content " >
< Link to = { `/streams/edit/ ${ stream . id } ` } className = " ui button primary " >
Edit
< /Link >
< Link to = { `/streams/delete/ ${ stream . id } ` } className = " ui button negative " >
Delete
< /Link >
< /div >
);
}
}
renderList () {
return this . props . streams . map ( stream => {
return (
< div className = " item " key = { stream . id } >
{ this . renderAdmin ( stream )}
< i className = " large middle aligned icon camera " />
< div className = " content " >
< Link to = { `/streams/ ${ stream . id } ` } className = " header " >
{ stream . title }
< /Link >
< div className = " description " > { stream . description } < /div >
< /div >
< /div >
);
})
}
renderCreate () {
if ( this . props . isSignedIn ) {
return (
< div style =>
< Link to = " /streams/new " className = " ui button primary " >
Create Stream
< /Link >
< /div >
);
}
}
render () {
//console.log(this.props.streams);
return (
< div >
< h2 > Streams < /h2 >
< div className = " ui celled list " > { this . renderList ()} < /div >
{ this . renderCreate ()}
< /div >
);
}
}
const mapStateToProps = ( state ) => {
return {
streams : Object . value ( state . streams ),
currentUserId : state . auth . userId ,
isSignedIn : state . auth . isSignedIn
}; //transform values of object into array
}
export default connect ( mapStateToProps , { fetchStreams })( StreamList );
client/src/components/streams/StreamCreate.js
import React from ' react ' ;
import { connect } from ' react-redux ' ;
import { createStream } from ' ../../actions ' ;
import { StreamForm } from ' ./StreamForm ' ;
class StreamCreate extends React . Component {
onSubmit = ( formValues ) => {
console . log ( formValues ); //submitted values
this . props . onSubmit ( formValues );
}
render () {
return (
< div >
< h3 > Create a Stream < /h3 >
< StreamForm onSubmit = { this . onSubmit } / >
< /div >
);
}
}
export default connect (
null ,
{ createStream }
)( SteamCreate );
client/src/components/streams/StreamDelete.js
import React from ' react ' ;
import { connect } from ' react-redux ' ;
import { Link } from ' react-router-dom ' ;
import Modal from ' ../Modal ' ;
import history from ' ../../history ' ;
import { fetchStream , deleteStream } from ' ../../actions ' ;
class StreamDelete extends React . Component {
componentDidMount () {
//console.log(this.props); //we get params from here
this . props . fetchStream ( this . props . match . params . id );
}
renderActions () {
const id = this . props . match . params . id ;
return (
< React . Fragment >
< button
onClick = {() => this . props . deleteStream ( id )}
className = " ui button negative "
>
Delete
< /button >
< Link to = " / " className = " ui button " >
Cancel
< /Link >
< /React.Fragment >
/*
or we can do this to produce empty element, if we dont want to use div
<>
<button className="ui button negative">Delete</button>
<button className="ui button">Cancel</button>
</>
*/
);
}
renderContent () {
if ( ! this . props . stream ) {
return ' Are you sure you want to delete this stream? ' ;
}
return `Are you sure you want to delete the stream with title: ${ this . props . stream . title } ` ;
}
render () {
return (
< Modal
title = " Delete Stream "
content = { this . renderContent ()}
actions = { this . renderActions ()}
onDismiss = {() => history . push ( ' / ' )}
/ >
);
}
}
mapStateToProps = ( state , ownProps ) => {
return {
stream : state . streams [ ownProps . match . params . id ]
}
}
export default connect ( mapStateToProps , { fetchStream , deleteStream })( StreamDelete );
client/src/components/streams/StreamEdit.js
imoprt _ from ' lodash ' ;
import React from ' react ' ;
import { connect } from ' react-redux ' ;
import { fetchStream , editStream } from ' ../../actions ' ;
import StreamForm from ' ./StreamForm ' ;
class StreamEdit extends React . Component {
componentDidMount () {
this . props . fetchStream ( this . props . match . params . id );
}
onSubmit = ( formValues ) => {
//console.log(formValues);
this . props . editStream ( this . props . match . params . id , formValues );
};
render () {
console . log ( this . props );
if ( ! this . props . stream ) {
return < div > Loading ... < /div >
}
return (
< div >
< h3 > Edit a Stream < /h3 >
//initialValues is special property of Redux Form
//this.props.stream is object with properties title and description
< StreamForm
initialValues = { _ . pick ( this . props . stream , ' title ' , ' description ' )}
//initialValues={ title: this.props.stream.title, description: this.props.streams.description}
onSubmit = { this . onSubmit }
/ >
< /div >
);
}
};
const mapStateToProps = ( state , ownProps ) => {
//state is redux store
console . log ( ownProps ); //these are props from props above
return {
stream : state . streams [ ownProps . match . params . id ]
}
}
export default connect ( mapStateToProps , { fetchStream , editStream })( StreamEdit );
client/src/components/streams/StreamShow.js
import React from ' react ' ;
import { connect } from ' react-redux ' ;
import { fetchStream } from ' ../../actions ' ;
class StreamShow extends React . Component {
componentDidMount () {
this . props . fetchStream ( this . props . match . params . id );
}
render () {
if ( ! this . props . stream ) {
return < div > Loading ... < /div >
}
return (
< div >
< h1 >
{ this . props . stream . title }
< /h1 >
< h5 >
{ this . props . stream . description }
< /h5 >
< /div >
);
}
}
const mapStateToProps = ( state , ownProps ) => {
return {
stream : state . streams [ ownProps . match . params . id ]
};
};
export default connect (
mapStateToProps ,
{ fetchStream }
)( StreamShow );
client/src/components/streams/StreamForm.js
import React from ' react ' ;
import { Field , reduxForm } from ' redux-form ' ;
class StreamForm extends React . Component {
renderError ({ error , touched }) { //this is destructuring object meta, so we will have access only to meta.error and meta.touched
if ( touched && error ) {
return (
< div className = " ui error message " >
< div className = " header " > { error } < /div >
< /div >
);
}
}
renderInput = ({ input , label , meta }) => {
console . log ( meta );
const className = `field ${ meta . error && meta . touched ? ' error ' : '' } ` ;
return (
< div className = " {className} " >
< label > { label } < /label >
< input {... input } autoComplete = " off " />
{ this . renderError ( meta )}
);
// code above is shortcut for this
// <input
// onChange={formProps.input.onChange}
// value={formProps.input.value}
// />
}
onSubmit = ( formValues ) => {
console . log ( formValues ); //submitted values
this . props . onSubmit ( formValues );
}
render () {
console . log ( this . props ); //check available functions of redux-form
return (
< form
onSubmit = { this . props . handleSubmit ( this . onSubmit )}
className = " ui form error "
>
< Field name = " title " component = { this . renderInput } label = " Enter Title " />
< Field name = " description " component = { this . renderInput } label = " Enter Description " />
< button className = " ui button primary " > Submit < /butto n
< /form >
);
}
}
const validate = ( formsValues ) => {
const errors = {};
if ( ! formValues . title ) {
errors . title = ' You must enter a title ' ;
}
if ( ! formValues . descriptin ) {
errors . title = ' You must enter a description ' ;
}
return errors ;
}
export default reduxForm ({
form : ' streamForm ' ,
validate : validate
})( StreamForm );
Actions
client/src/actions/types.js
export const SIGN_IN = ' SIGN_IN ' ;
export const SIGN_OUT = ' SIGN_OUT ' ;
export const CREATE_STREAM = ' CREATE_STREAM ' ;
export const FETCH_STREAMS = ' FETCH_STREAMS ' ;
export const FETCH_STREAM = ' FETCH_STREAM ' ;
export const DELETE_STREAM = ' DELETE_STREAM ' ;
export const EDIT_STREAM = ' EDIT_STREAM ' ;
client/src/actions/index.js
import streams from ' ../apis/streams ' ;
import history from ' ../history ' ;
import {
SIGN_IN ,
SIGN_OUT ,
CREATE_STREAM ,
FETCH_STREAM ,
DELETE_STREAM ,
EDIT_STREAM
} from ' ./types ' ;
export const signIn = ( userId ) => {
return {
type : SIGN_IN ,
payload : userId
};
};
export const signOut = () => {
return {
type : SIGN_OUT
};
};
export const createStream = ( formValues ) => {
//everytime we want to do async request, we are using redux-thunk
return async ( dispatch , getState ) => {
const { userId } = getState (). auth ;
const response = await streams . post ( ' /streams ' , {... formValues , userId });
dispatch ({ type : CREATE_STREAM , payload : response . data });
history . push ( ' / ' );
}
};
export const fetchStreams = () => async dispatch => {
const response = await streams . get ( ' /streams ' );
dispatch ({ type : FETCH_STREAMS , payload : response . data });
};
export const fetchStream = ( id ) => async dispatch => {
const response = await streams . get ( `/streams/{$id}` );
dispatch ({ type : FETCH_STREAM , payload : response . data });
};
export const editStream = ( id , formValues ) => async dispatch => {
const response = await streams . patch ( `/streams/{$id}` , formValues );
dispatch ({ type : EDIT_STREAM , payload : response . data });
history . push ( ' / ' );
};
export const deleteStream = ( id ) => async dispatch => {
await streams . delete ( `/streams/ ${ id } ` );
dispatch ({ type : DELETE_STREAM , payload : id });
history . push ( ' / ' );
};
Reducers
client/src/reducers/index.js
import { combineReducers } from ' redux ' ;
import { reducer as formReducer } from ' redux-form ' ;
import authReducer from ' ./authReducer ' ;
import streamReducer from ' ./streamReducer ' ;
export default combineReducers ({
auth : authReducer ,
form : formReducer ,
streams : streamReducer
});
client/src/reducers/authReducer.js
import { SIGN_IN , SIGN_OUT } from ' ./types ' ;
const INITIAL_STATE = {
isSignedIn : null ,
userId : null
}
export default ( state = INITIAL_STATE , action ) => {
switch ( action . type ) {
case SIGN_IN :
return { ... state , isSignedIn : true , userId : action . payload };
case SIGN_OUT :
return { ... state , isSignedIn : false , userId : null };
default :
return state ;
}
}
client/src/reducers/streamReducer.js
import _ from ' lodash ' ;
import {
FETCH_STREAM ,
FETCH_STREAMS ,
CREATE_STREAM ,
EDIT_STREAM ,
DELETE_STREAM
} from ' ../actions/types ' ;
export default ( state = {}, action ) => {
switch ( action . type ) {
case ' FETCH_STREAMS ' :
return {... state , ... _ . mapKeys ( action . payload , ' id ' )};
case ' FETCH_STREAM ' :
return {... state , [ action . payload . id ]: action . payload };
case ' CREATE_STREAM ' :
return {... state , [ action . payload . id ]: action . payload };
case ' EDIT_STREAM ' :
return {... state ,. [ action . payload . id ]: action . payload };
case ' DELETE_STREAM ' :
return _ . omit ( state , action . payload );
default :
return state ;
}
};
history.js
client/src/history/history.js
import { createBrowserHistory } from ' history ' ;
export default createBrowserHistory ();
index.html
index.html
...
< div id = " root " >< /div >
<!-- modal popup window -->
< div id = " modal " >< /div >
....