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</button
            </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>
....