Cosmic JS Blog Stay tuned for community news, company announcements and updates from the Cosmic JS team.

How to Build a Sticky Notes App Using React and Redux


In this tutorial, I'm going to show you how to create a simple Sticky Notes app using React, Redux, Selectors, Redux Sagas, a little bit of Node, and Cosmic JS. For the sake of understanding how to consume Restful API’s, this tutorial will show how to make AJAX (XHR) requests to the Cosmic JS API in order to retrieve, add, update, and delete data/media in our Cosmic JS Buckets. Let's get started.

TL;DR

Download the GitHub repo
Check out the demo

{
  "name": "sticky-notes",
  "version": "1.0.0",
  "description": "Sticky Notes app built using react, redux & cosmic.",
  "main": "index.js",
  "scripts": {
    "start": "npm run build; node server/index.js",
    "start-dev": "nodemon server/index.js",
    "build": "webpack -p",
    "build-dev": "webpack -w",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/jazibsawar/sticky-notes.git"
  },
  "author": "Muhammad Musa",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/jazibsawar/sticky-notes/issues"
  },
  "homepage": "https://github.com/jazibsawar/sticky-notes#readme",
  "dependencies": {
    "async": "^2.5.0",
    "babel": "^6.23.0",
    "babel-core": "^6.25.0",
    "babel-loader": "^7.1.1",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-plugin-transform-regenerator": "^6.24.1",
    "babel-polyfill": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "body-parser": "^1.17.2",
    "cosmicjs": "^2.39.91",
    "express": "^4.15.3",
    "immutable": "^3.8.1",
    "moment": "^2.18.1",
    "prop-types": "^15.5.10",
    "react": "^15.6.1",
    "react-addons-css-transition-group": "^15.6.0",
    "react-addons-transition-group": "^15.6.0",
    "react-color": "^2.13.4",
    "react-dom": "^15.6.1",
    "react-modal-dialog": "^4.0.7",
    "react-redux": "^5.0.5",
    "react-router": "^3.0.5",
    "react-spinners": "0.0.30",
    "redux": "^3.7.2",
    "redux-logger": "^3.0.6",
    "redux-saga": "^0.15.6",
    "redux-thunk": "^2.2.0",
    "reselect": "^3.0.1",
    "styled-components": "^2.1.1",
    "volleyball": "^1.4.1",
    "webpack": "^3.4.1",
    "webpack-livereload-plugin": "^0.11.0",
    "whatwg-fetch": "^2.0.3"
  },
  "devDependencies": {
    "chai": "^4.1.0",
    "cross-env": "^5.0.1",
    "mocha": "^3.5.0",
    "nodemon": "^1.11.0"
  }
}

What we're installing and why:

  1. We're going to use the whatwg-fetch and cosmicjs library to handle our requests to our Cosmic JS Bucket.
  2. We're installing react and react-dom to build our react components.
  3. We are going to use redux, react-redux, redux-logger, reselect and redux-sagas to help us implement what is called the Redux architecture.
  4. We have used styled-components to make the stickynotes component styling generic. We can pass style variables in it too.
  5. We used react-spinners to use loading component.
  6. We used react-color to use color picker component.
  7. We used react-modal-dialog to use react modal component.
  8. We used immutable package by Facebook to use immutable objects.
  9. We used moment to format dates.
  10. We used react-router for routing and navigation.
  11. The only thing worth mentioning in the dev dependencies is webpack and volleyball. Webpack will help us bundle all of our react and redux files into one large "bundle" file that will be used in our index.html. Volleyball is a cool little library that allows us to see in our console incoming and outgoing HTTP requests.

Building our app:

Now we we will set up our index.html in our client directory. Copy and paste the following code into your index.html file:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Cosmic Sticky Notes App!</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" >
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
    <script src="/dist/bundle.js" defer></script>
</head>
<body id="top">
<div id="root"></div>
</body>
</html>

Here, we are going to target our root div to place our react components in later. The bundle.js file located in our dist directory is what our webpack.config file will spit out after bundling all of our react components. Now, set up our webpack.config file to bundle all of our react files and export that bundle file to our dist directory. Copy the following code into your webpack.config.js file:

var path = require('path');
var LiveReloadPlugin = require('webpack-livereload-plugin');
var webpack = require('webpack');

module.exports = {
  entry: './client/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'client/dist')
  },
  context: __dirname,
  resolve: {
    extensions: ['.js', '.jsx', '.json', '*']
  },
  module: {
    rules: [{
        test: /\.jsx?$/,
        exclude: /(node_modules|bower_components)/,
        loader: 'babel-loader',
        options: {
          presets: ['react', 'es2015', 'stage-0']
        }
      },
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'COSMIC_BUCKET': JSON.stringify(process.env.COSMIC_BUCKET),
        'COSMIC_READ_KEY': JSON.stringify(process.env.COSMIC_READ_KEY),
        'COSMIC_WRITE_KEY': JSON.stringify(process.env.COSMIC_WRITE_KEY)
      }
    }),
    new LiveReloadPlugin({appendScriptTag: true}),
  ]
};

Then, we have server/index.js file when we used express to make our server.

In Client folder there is store.js file which is creating redux store to manage states and data. The things that are happening are as follows:

  • We used redux-thunk & redux-sagas middlewares.
  • We used combine Reducers to combine home & group reducer
  • Then we made store using the above things.
  • Then we trigger home & groups sagas.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import homeReducer from './containers/Home/reducer';
import homeSagas from './containers/Home/sagas';


import groupReducer from './containers/Group/reducer';
import groupSagas from './containers/Group/sagas';

import thunkMiddleware from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware();

const middlewares = [
  sagaMiddleware,
  thunkMiddleware,
];

const reducers = combineReducers({
  home: homeReducer,
  group: groupReducer,
});
const store = createStore(
  reducers,
  applyMiddleware(...middlewares),
);

sagaMiddleware.run(homeSagas)
sagaMiddleware.run(groupSagas)

export default store;

Now we have client/Routes.js. Here, we have two routes:

  1. Path="/" => Lists all groups
  2. Path="/group/:groupSlug/:groupId" => list all notes against some groups
import React from 'react';
import { Router, Route, browserHistory } from 'react-router';
import { connect } from 'react-redux';
import Home from './containers/Home';
import Group from './containers/Group';

const Routes = ({}) => {
  return (
    <Router history={browserHistory}>
        <Route path="/" component={Home} />
        <Route path="/group/:groupSlug/:groupId" component={Group} />
    </Router>
  )
};

// const mapState = ({}) => ({});
// const mapDispatch = {};

export default connect(null, null)(Routes);

In client folder we have create a configuration file inorder to save API_KEYS and COSMIC BUCKET NAME. You can edit this file to use your own keys and slugs.

export default {
  bucket: {
    slug: process.env.COSMIC_BUCKET || 'sticky-note-app',
    read_key: "YOUR READ KEY",
    write_key: "YOUR WRITE KEY",
  }
}

There are two utils:

  • Request
  • Cosmic

Request

Request uses whatwg-fetch to hit AJAX requests to the Cosmic JS API. This util is used to call those endpoints which are not present in the Cosmic JS package e.g deleteMedia and searchObjects based on filters

Cosmic JS

Cosmic JS uses cosmicjs package to hit AJAX request to Cosmic API.

There are two container:

  • Home
  • Group

Home

This container lists down groups. This group add new Note Group. It also edits and delete NoteGroups. This selects NoteGroup data from reducers and passes them to their components. This also passes add, edit and delete NoteGroup actions to the home component.

Group

This selects Notes data from reducers and passes them to their components. This also passes add, edit and delete Notes actions to the home component.

There are following components in this app

  • Color Picker
  • Dialog
  • Loader
  • Sticky Notes
  • Home
  • Group

ColorPicker

This component is for picking the color of Sticky Note.

import React, {Component} from 'react';
import { ChromePicker } from 'react-color';


class ColorPickerField extends Component {
  constructor(props){
    super(props);
  }

  render() {
    return (
      <ChromePicker
        color={this.props.color}
        onChangeComplete={this.props.changeColor}
      />
    )
  }
}

export default ColorPickerField;

Dialog

This component will show the popups and dialogs to get input data and output responses

import React, {Component} from 'react';
import { ModalContainer, ModalDialog } from 'react-modal-dialog';


class Dialog extends Component {
  constructor(props){
    super(props);
  }


  handleClose = () => {
    this.props.closeDialog();
  }
  render() {
    const { open } = this.props;
    return (
      <div>
        {
          open &&
          <ModalContainer onClose={this.handleClose}>
            <ModalDialog onClose={this.handleClose}>
              { this.props.children }
            </ModalDialog>
          </ModalContainer>
        }
      </div>
    )
  }
}

export default Dialog;

Loader

This is a loader component and it triggers whenever an ajax request in taking place.

import React, {Component} from 'react';
import { BounceLoader } from 'react-spinners';


class Loader extends Component {
  constructor(props){
    super(props);
  }


  render() {
    const style = {
      position: "fixed",
      top: "50%",
      left: "50%",
      marginTop: "-9em",
      marginLeft: "-15em",
    };
    return (
      <div style={style}>
        <BounceLoader
          color={'#123abc'}
          loading
          size={300}
        />
      </div>
    )
  }
}

export default Loader;

StickyNotes

This component takes an array of options and convert them to stickynotes with edit, heading onclick and delete handler.

import React, {Component} from 'react';
import { NoteListWrapper, NoteWrapper, Note } from './styles';
import moment from 'moment/moment'
class StickyNotes extends Component {


  render() {
    const { options } = this.props;
    return (
      <NoteListWrapper>
        {
          !!options && options.toArray().map((option, index) => {
          return  <NoteWrapper key={`note_${index}`}>
              <Note backgroundColor={option.getIn(['metadata', 'color'])||"black"} color="#ffffff" rotate={index%2 === 0? -2 : 4}>
                <small>{moment(option.get('created')).fromNow()}</small>
                <h4 onClick={() => this.props.handleClick(option)}>{option.get('title').substring(0,11)}</h4>
                <p>{option.get('content').substring(0, 41)}</p>
                <span className="buttons">
                  <a onClick={() => this.props.deleteOption(option.toJS(), index)} className="pull-right"><i className="fa fa-trash-o"></i></a>
                  <a onClick={() => this.props.editOption(option, index)} style={{ marginRight: "1vw" }} className="pull-right"><i className="fa fa-pencil"></i></a>
                </span>
              </Note>
            </NoteWrapper>
          })
        }
      </NoteListWrapper>
    )
  }
}

export default StickyNotes;

Home

This is a component where we feed NoteGroup options to Sticky Notes component and have add, edit and delete handlers.

import React, {Component} from 'react';
import Dialog from '../Dialog';
import StickyNotes from '../StickyNotes';
import ColorPicker from '../ColorPicker';

class Home extends Component {
  constructor(props){
    super(props);
    this.state = {
      title: "",
      content: "",
      color: "#fff",
      selectedGroup: null,
      openAddDialog: false,
      openEditDialog: false,
      group: {
        title: "",
      },
    }
  }

  addGroup = () => {
    const { title, content, color } = this.state;
    this.props.addGroup({
      title,
      content,
      color,
    });
    this.setState({ title: "", content: "", openAddDialog: false })
  }


  editGroup = () => {
    const { group, selectedGroup } = this.state;
    const { state } = this;
    this.props.editGroup({
      title: group.title,
      content: group.content,
      color: group.color,
    }, group.slug, selectedGroup);
    this.setState({ ...state, group: { title: "" }, openEditDialog: false })
  }

  goToNoteGroup = (group) => {
    this.props.changeRoute(`/group/${group.get('slug')}/${group.get('_id')}`);
  }

  editOption = (group, selectedGroup) => {
    this.setState({
      openEditDialog: true,
      group: group.toJS(),
      selectedGroup,
    });
  }
  render() {
    const { groups } = this.props;
    const { state } = this;
    const { title, content, color, openAddDialog, openEditDialog, group } = this.state;

    const styles = {
      btnCircle: {
        width: "30px",
        height: "30px",
        textAlign: "center",
        padding: "6px 0",
        fontSize: "12px",
        fontWeight: "700",
        lineHeight: "1.42",
        borderRadius: "15px",
        margin: "10px",
        border: "none",
        outline: "none",
        float: "right"
      }
    }

    return (
      <div className="container-fluid">
      <div className="row">
        <button style={styles.btnCircle} className="btn btn-primary btn-lg" onClick={() => this.setState({ openAddDialog: true })}>
          <i className="fa fa-plus"></i>
        </button>
      <Dialog
        open={openAddDialog}
        closeDialog={() => this.setState({ openAddDialog: false })}
      >
        <div className="container-fluid">
          <div className="col-xs-12">
            <input placeholder="Enter Name ..." type="text" style={{ margin: "1vh 0" }} value={title} className="form-control" onChange={(e) => this.setState({ title: e.target.value })} />
          </div>
          <div className="col-xs-12">
            <textarea value={content} placeholder="Enter Description ..." rows="4" cols="50" style={{ margin: "1vh 0" }} className="form-control" onChange={(e) => this.setState({ content: e.target.value })} />
          </div>

          <div className="col-xs-12">
            <ColorPicker color={color} changeColor={(color) => this.setState({ color: color.hex })} />
          </div>


          <div className="col-xs-12">
            <input type="button" disabled={title === "" && "disabled"} style={{ margin: "1vh 0" }} value="Add Group" className="btn btn-primary btn-lg" onClick={this.addGroup} />
          </div>
        </div>
      </Dialog>

      <Dialog
        open={openEditDialog}
        closeDialog={() => this.setState({ openEditDialog: false })}
      >
        <div className="container-fluid">
          <div className="col-xs-12">
            <input style={{ margin: "1vh 0" }} type="text" value={group.title} className="form-control" onChange={(e) => this.setState({ ...state, group: {  ...this.state.group, title: e.target.value } })} />
          </div>
          <div className="col-xs-12">
            <textarea value={group.content||""} style={{ margin: "1vh 0" }} type="text" className="form-control" onChange={(e) => this.setState({ ...state, group: {  ...this.state.group, content: e.target.value } })} />
          </div>
          <div className="col-xs-12">
            <ColorPicker color={group.color} changeColor={(color) => this.setState({ ...state, group: { ...this.state.group, color: color.hex }})} />
          </div>
          <div className="col-xs-12">
            <input disabled={group.title === "" && "disabled"} style={{ margin: "1vh 0" }} type="button" value="Edit Group" className="btn btn-warning btn-lg" onClick={this.editGroup} />
          </div>

        </div>
      </Dialog>

      {
        <StickyNotes
          options={groups}
          editOption={this.editOption}
          deleteOption={this.props.deleteGroup}
          handleClick={this.goToNoteGroup}
        />
      }
      </div>
      </div>
    )
  }
}

export default Home;

Group

This is a component where we feed Notes options to StickyNotes component and have add, edit and delete handlers.

import React, {Component} from 'react';
import Dialog from '../Dialog';
import StickyNotes from '../StickyNotes';
import ColorPicker from '../ColorPicker';

class Group extends Component {
  constructor(props){
    super(props);
    this.state = {
      title: "",
      content: "",
      color: "",
      selectedNote: null,
      openAddDialog: false,
      openEditDialog: false,
      openViewDialog: false,
      note: {
        title: "",
      },
    }
  }

  addNote = () => {
    const { title, color, content } = this.state;
    const { groupId, addedMedia } = this.props;
    const image = this.refs.imageFile.files[0];
    const file = this.refs.attachedFile.files[0];
    this.props.addNote({
      title,
      content,
      color,
      image,
      file,
    }, groupId);
    this.setState({ title: "", content: "", openAddDialog: false });
  }

  editNote = () => {
    const { note, selectedNote } = this.state;
    const { state } = this;
    let METAFIELDS = note;
    METAFIELDS.metafields[1].value = note.color || "#000"
    this.props.editNote({
      title: note.title,
      color: note.color || "#000",
      content: note.content,
      metafields: METAFIELDS.metafields,
    }, note.slug, selectedNote);
    this.setState({ ...state, note: { title: "" }, openEditDialog: false })
  }

  editOption = (note, selectedNote) => {
    this.setState({
      openEditDialog: true,
      note: note.toJS(),
      selectedNote,
    });
  }

  goToNote = (note) => {
    this.setState({
      note: note.toJS(),
      openViewDialog: true,
    })
  }
  render() {
    const { notes } = this.props;
    const { state } = this;

    const { title, content, color, openAddDialog, openEditDialog, openViewDialog, note } = this.state;

    const styles = {
      btnCircle: {
        width: "30px",
        height: "30px",
        textAlign: "center",
        padding: "6px 0",
        fontSize: "12px",
        fontWeight: "700",
        lineHeight: "1.42",
        borderRadius: "15px",
        margin: "10px",
        border: "none",
        outline: "none",
        float: "right"
      }
    }

    return (
      <div className="container-fluid">
      <button style={styles.btnCircle}  onClick={() => this.setState({ openAddDialog: true })} className="btn btn-primary btn-lg">
        <i className="fa fa-plus"></i>
      </button>
      <Dialog
        open={openAddDialog}
        closeDialog={() => this.setState({ openAddDialog: false })}
      >
        <div className="container-fluid">
          <div className="col-xs-12">
            <input style={{ margin: "1vh 0" }} type="text" className="form-control" value={title} onChange={(e) => this.setState({ title: e.target.value })} /> <br />
          </div>
          <div className="col-xs-12">
            <textarea style={{ margin: "1vh 0" }} type="text" className="form-control" value={content} onChange={(e) => this.setState({ content: e.target.value })} /> <br />
          </div>
          <div className="col-xs-12">
            <input style={{ margin: "1vh 0" }} type="file" className="form-control" ref="imageFile" /> <br />
          </div>
          <div className="col-xs-12">
            <input style={{ margin: "1vh 0" }} type="file" className="form-control" ref="attachedFile" /> <br />
          </div>
          <div className="col-xs-12">
            <ColorPicker color={color} changeColor={(color) => this.setState({ color: color.hex })} />
          </div>
          <div className="col-xs-12">
            <input disabled={(title === "" || content === "") && "disabled"} style={{ margin: "1vh 0" }} type="button" className="btn btn-success btn-md" value="Add Note" onClick={this.addNote} />
          </div>
        </div>
      </Dialog>



      <Dialog
        open={openEditDialog}
        closeDialog={() => this.setState({ openEditDialog: false })}
      >
        <div className="container-fluid">
          <div className="col-xs-12">
            <input style={{ margin: "1vh 0" }} type="text" value={note.title} className="form-control" onChange={(e) => this.setState({ ...state, note: {  ...this.state.note, title: e.target.value } })} />
          </div>
          <div className="col-xs-12">
            <textarea style={{ margin: "1vh 0" }} type="text" value={note.content||""} className="form-control" onChange={(e) => this.setState({ ...state, note: {  ...this.state.note, content: e.target.value } })} />
          </div>
          <div className="col-xs-12">
            <ColorPicker color={note.color} changeColor={(color) => this.setState({ ...state, note: { ...this.state.note, color: color.hex }})} />
          </div>
          <div className="col-xs-12">
            <input disabled={(note.title === "" || note.content === "") && "disabled"} style={{ margin: "1vh 0" }} type="button" value="Edit Note" className="btn btn-warning btn-lg" onClick={this.editNote} />
          </div>
        </div>
      </Dialog>

      <Dialog
        open={openViewDialog}
        closeDialog={() => this.setState({ openViewDialog: false })}
      >
        <div className="container-fluid">
          <div className="col-xs-12">
            <h1 style={{ margin: "1vh 0" }}>{note.title}</h1>
          </div>
          <div className="col-xs-12">
            <p style={{ margin: "1vh 0" }}>{note.content}</p>
          </div>
          {
            !!note.metadata && !!note.metadata.feature_image &&
            <div className="col-xs-12">
              <img style={{ margin: "1vh 0" }} width="128" height="128" src={note.metadata.feature_image.imgix_url} />
            </div>
          }
          {
            !!note.metadata && !!note.metadata.attachment &&
            <div className="col-xs-12">
              <a style={{ margin: "1vh 0" }} href={note.metadata.attachment.imgix_url} target="_blank">Click on this link to open attachment in the new link</a>
            </div>
          }
        </div>
      </Dialog>

      <StickyNotes
        options={notes}
        editOption={this.editOption}
        deleteOption={this.props.deleteNote}
        handleClick={this.goToNote}
      />
      </div>
    )
  }
}

export default Group;

Conclusion

So, this is an app where every scenario of the Cosmic JS RESTful API is covered. I hope you liked the tutorial.

You may also like


Download receipts for your Cosmic JS Buckets within your Dashboard. 

Brand Managers manage, well, brands.  Seems simple enough right?  It does in title and theory alone.  Managing a brand means managing the brand’s tone, voice, messaging, consumer segmentation, price points, marketing, advertising and all of the subsequent elements and assets that fall out of such an engagement.  Having managed brands for years myself, I can attest to the magnitude of the job at hand.  Once the client has agreed upon a general direction, it becomes the brand manager’s responsibility to ignite passion and results from the internal agency team that services the account.  As my workload would increase month over month as business heated up or an account was grown, I went through the whole Automation | Delegation | Elimination routine to see what I could cut out of my schedule as busy work.  The problem?  I was only one person.  I could only affect hours tallies on my end, but had little to no influence over design, development, copywriting & production. 


The area that always seemed to be the weak link in terms of staying on budget was in development.  Boutique agencies struggle to attract qualified talent with their long work hours and subpar compensation, and then after taking a gamble on a more junior talent that is developed over time, retention is a beating.  Companies line up with development jobs that pay 2X, sometimes 3X what a boutique agency is willing to part with for that developer’s compensation package.  This is all before taking into account staging servers, hosting servers, CMS logins, local installations of CMS systems and all of the red tape and bureaucracy associated that can bog down a productive workflow.  As a result, my quotes back to clients for websites, microsite, landing pages and applications were always a bit higher than they were expecting.  I started searching for a cloud-based solution to my CMS woes to cut out some of the middle men and see if there was an easier way to 'get this digital property live'. 

I found 
Cosmic JS.  Had I been told as a Brand Manager that I could eliminate the local CMS, the hosting server and the shared logins of content editing, I would have seen the value immediately.  No longer having to build APIs on a per-CMS / per-client basis, no longer having to build out a proprietary backend, yet still attaining the same custom-value would have been a lifesaver and a half for a brand manager focused on the bottom line.  It would have easily cut my back end developers’ hours estimates by 40%, eliminated costly hosting servers and would have streamlined content-centric employees within the agency to not have to deal with the red tape of updating content within a traditional CMS. 

Whether we picked out a
content-ready application or plugged GitHub into Cosmic JS, I’m seeing time and cost savings at every turn.  Music to a brand manager’s ears, and music to a boutique agency’s margin and bottom line.  As it turns out, it also benefits the client as their content is put first, their content is pushed live more quickly and is devourable globally on any device.

We're excited to announce a new feature that allows you to easily monitor API usage and activity for your Cosmic JS-powered applications.

We're on a mission to help teams of all sizes collaborate to build apps faster and easier.  In some cases incorporating a new solution into a workflow for larger team involves challenges for user onboarding and management.

We're happy to announce that we've rolled out some new features requested by members of our community.  Here are some of the new features:


Cosmic JS Founders Tony Spiro and Carson Gibbons were featured on The Next Level Show, a Dallas-based business talk show that features Founders and CEOs. The founders discussed their historical pain points with installed content management systems, as well as their partnership, private beta updates, and the frontier of cloud-based CMS APIs that both speed up the development cycle and add to the bottom line. Stream the audio file or watch the interview here.