diff --git a/src/components/forms/validations.jsx b/src/components/forms/validations.jsx
index c83ce63cb..c882aa40d 100644
--- a/src/components/forms/validations.jsx
+++ b/src/components/forms/validations.jsx
@@ -28,8 +28,8 @@ module.exports.validationHOCFactory = defaultValidationErrors => (Component => {
         <Component
             validationErrors={defaults(
                 {},
-                defaultValidationErrors,
-                props.validationErrors
+                props.validationErrors,
+                defaultValidationErrors
             )}
             {...omit(props, ['validationErrors'])}
         />
diff --git a/src/components/modal/addtostudio/modal.scss b/src/components/modal/addtostudio/modal.scss
index 7503271c6..08b4c1abc 100644
--- a/src/components/modal/addtostudio/modal.scss
+++ b/src/components/modal/addtostudio/modal.scss
@@ -35,7 +35,6 @@
 .addToStudio-modal-content {
     margin: 0 auto;
     width: 100%;
-    line-height: 1.5rem;
     font-size: .875rem;
 }
 
diff --git a/src/components/modal/base/modal.scss b/src/components/modal/base/modal.scss
index 1a6e46262..cb878ae12 100644
--- a/src/components/modal/base/modal.scss
+++ b/src/components/modal/base/modal.scss
@@ -64,11 +64,20 @@ $modal-close-size: 1rem;
 .action-buttons {
     display: flex;
     margin: 1.125rem .8275rem .9375rem .8275rem;
+    line-height: 1.5rem;
     justify-content: flex-end !important;
     align-items: flex-start;
     flex-wrap: nowrap;
 }
 
+/* setting overall modal to contain overflow looks good, but isn't
+compatible with elements (like validation popups) that need to bleed
+past modal boundary. This class can be used to force modal button
+row to appear to contain overflow. */
+.action-buttons-overflow-fix {
+    margin-bottom: .9375rem;
+}
+
 .action-button {
     margin: 0 0 0 .54625rem;
     border-radius: .25rem;
@@ -83,3 +92,19 @@ $modal-close-size: 1rem;
 .action-button-text {
     display: flex;
 }
+
+.action-button.disabled {
+    background-color: $active-dark-gray;
+}
+
+.error-text
+{
+    display: block;
+    border: 1px solid $active-gray;
+    border-radius: 5px;
+    background-color: $ui-orange;
+    padding: 1rem;
+    min-height: 1rem;
+    overflow: visible;
+    color: $type-white;
+}
diff --git a/src/components/modal/report/modal.jsx b/src/components/modal/report/modal.jsx
index 7b78379a0..7271458fd 100644
--- a/src/components/modal/report/modal.jsx
+++ b/src/components/modal/report/modal.jsx
@@ -1,10 +1,12 @@
 const bindAll = require('lodash.bindall');
 const PropTypes = require('prop-types');
 const React = require('react');
+const connect = require('react-redux').connect;
 const FormattedMessage = require('react-intl').FormattedMessage;
 const injectIntl = require('react-intl').injectIntl;
 const intlShape = require('react-intl').intlShape;
 const Modal = require('../base/modal.jsx');
+const classNames = require('classnames');
 
 const Form = require('../../forms/form.jsx');
 const Button = require('../../forms/button.jsx');
@@ -12,6 +14,7 @@ const Select = require('../../forms/select.jsx');
 const Spinner = require('../../spinner/spinner.jsx');
 const TextArea = require('../../forms/textarea.jsx');
 const FlexRow = require('../../flex-row/flex-row.jsx');
+const previewActions = require('../../../redux/preview.js');
 
 require('../../forms/button.scss');
 require('./modal.scss');
@@ -68,12 +71,24 @@ class ReportModal extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
-            'handleReportCategorySelect'
+            'handleCategorySelect',
+            'handleValid',
+            'handleInvalid'
         ]);
-        this.state = {reportCategory: this.props.report.category};
+        this.state = {
+            category: '',
+            notes: '',
+            valid: false
+        };
     }
-    handleReportCategorySelect (name, value) {
-        this.setState({reportCategory: value});
+    handleCategorySelect (name, value) {
+        this.setState({category: value});
+    }
+    handleValid () {
+        this.setState({valid: true});
+    }
+    handleInvalid () {
+        this.setState({valid: false});
     }
     lookupPrompt (value) {
         const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
@@ -82,17 +97,24 @@ class ReportModal extends React.Component {
     render () {
         const {
             intl,
+            isConfirmed,
+            isError,
+            isOpen,
+            isWaiting,
             onReport, // eslint-disable-line no-unused-vars
-            report,
+            onRequestClose,
             type,
             ...modalProps
         } = this.props;
+        const submitEnabled = this.state.valid && !isWaiting;
+        const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
         const contentLabel = intl.formatMessage({id: `report.${type}`});
         return (
             <Modal
                 className="mod-report"
                 contentLabel={contentLabel}
-                isOpen={report.open}
+                isOpen={isOpen}
+                onRequestClose={onRequestClose}
                 {...modalProps}
             >
                 <div>
@@ -104,68 +126,115 @@ class ReportModal extends React.Component {
 
                     <Form
                         className="report"
-                        onSubmit={onReport}
+                        onInvalid={this.handleInvalid}
+                        onValid={this.handleValid}
+                        onValidSubmit={onReport}
                     >
                         <div className="report-modal-content">
-                            <FormattedMessage
-                                id={`report.${type}Instructions`}
-                                values={{
-                                    CommunityGuidelinesLink: (
-                                        <a href="/community_guidelines">
-                                            <FormattedMessage id="report.CommunityGuidelinesLinkText" />
-                                        </a>
-                                    )
-                                }}
-                            />
-                            <Select
-                                required
-                                elementWrapperClassName="report-modal-field"
-                                label={null}
-                                name="report_category"
-                                options={REPORT_OPTIONS.map(option => ({
-                                    value: option.value,
-                                    label: this.props.intl.formatMessage(option.label)
-                                }))}
-                                value={this.state.reportCategory}
-                                onChange={this.handleReportCategorySelect}
-                            />
-                            <TextArea
-                                required
-                                className="report-text"
-                                elementWrapperClassName="report-modal-field"
-                                label={null}
-                                name="notes"
-                                placeholder={this.lookupPrompt(this.state.reportCategory)}
-                                validationErrors={{
-                                    maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
-                                    minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
-                                }}
-                                validations={{
-                                    maxLength: 500,
-                                    minLength: 20
-                                }}
-                                value={report.notes}
-                            />
+                            {isConfirmed ? (
+                                <div className="received">
+                                    <div className="received-header">
+                                        <FormattedMessage id="report.receivedHeader" />
+                                    </div>
+                                    <FormattedMessage id="report.receivedBody" />
+                                </div>
+                            ) : (
+                                <div>
+                                    <div className="instructions">
+                                        <FormattedMessage
+                                            id={`report.${type}Instructions`}
+                                            key={`report.${type}Instructions`}
+                                            values={{
+                                                CommunityGuidelinesLink: (
+                                                    <a href="/community_guidelines">
+                                                        <FormattedMessage id="report.CommunityGuidelinesLinkText" />
+                                                    </a>
+                                                )
+                                            }}
+                                        />
+                                    </div>
+                                    <Select
+                                        required
+                                        elementWrapperClassName="report-modal-field"
+                                        label={null}
+                                        name="report_category"
+                                        options={REPORT_OPTIONS.map(option => ({
+                                            value: option.value,
+                                            label: this.props.intl.formatMessage(option.label),
+                                            key: option.value
+                                        }))}
+                                        validationErrors={{
+                                            isDefaultRequiredValue: this.props.intl.formatMessage({
+                                                id: 'report.reasonMissing'
+                                            })
+                                        }}
+                                        value={this.state.category}
+                                        onChange={this.handleCategorySelect}
+                                    />
+                                    <TextArea
+                                        required
+                                        className="report-text"
+                                        elementWrapperClassName="report-modal-field"
+                                        label={null}
+                                        name="notes"
+                                        placeholder={this.lookupPrompt(this.state.category)}
+                                        validationErrors={{
+                                            isDefaultRequiredValue: this.props.intl.formatMessage({
+                                                id: 'report.textMissing'
+                                            }),
+                                            maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
+                                            minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
+                                        }}
+                                        validations={{
+                                            maxLength: 500,
+                                            minLength: 20
+                                        }}
+                                        value={this.state.notes}
+                                    />
+                                </div>
+                            )}
+                            {isError && (
+                                <div className="error-text">
+                                    <FormattedMessage id="report.error" />
+                                </div>
+                            )}
                         </div>
                         <FlexRow className="action-buttons">
-                            {report.waiting ? [
-                                <Button
-                                    className="submit-button"
-                                    disabled="disabled"
-                                    key="submitButton"
-                                    type="submit"
-                                >
-                                    <Spinner />
-                                </Button>
-                            ] : [
-                                <Button
-                                    className="submit-button"
-                                    key="submitButton"
-                                    type="submit"
-                                >
-                                    <FormattedMessage id="report.send" />
-                                </Button>
-                            ]}
+                            <div className="action-buttons-overflow-fix">
+                                {isConfirmed ? (
+                                    <Button
+                                        className="action-button submit-button"
+                                        type="button"
+                                        onClick={onRequestClose}
+                                    >
+                                        <div className="action-button-text">
+                                            <FormattedMessage id="general.close" />
+                                        </div>
+                                    </Button>
+                                ) : (
+                                    <Button
+                                        className={classNames(
+                                            'action-button',
+                                            'submit-button',
+                                            {disabled: !submitEnabled}
+                                        )}
+                                        {...submitDisabledParam}
+                                        key="submitButton"
+                                        type="submit"
+                                    >
+                                        {isWaiting ? (
+                                            <div className="action-button-text">
+                                                <Spinner mode="smooth" />
+                                                <FormattedMessage id="report.sending" />
+                                            </div>
+                                        ) : (
+                                            <div className="action-button-text">
+                                                <FormattedMessage id="report.send" />
+                                            </div>
+                                        )}
+                                    </Button>
+                                )}
+                            </div>
                         </FlexRow>
                     </Form>
                 </div>
@@ -176,15 +245,26 @@ class ReportModal extends React.Component {
 
 ReportModal.propTypes = {
     intl: intlShape,
+    isConfirmed: PropTypes.bool,
+    isError: PropTypes.bool,
+    isOpen: PropTypes.bool,
+    isWaiting: PropTypes.bool,
     onReport: PropTypes.func,
     onRequestClose: PropTypes.func,
-    report: PropTypes.shape({
-        category: PropTypes.string,
-        notes: PropTypes.string,
-        open: PropTypes.bool,
-        waiting: PropTypes.bool
-    }),
     type: PropTypes.string
 };
 
-module.exports = injectIntl(ReportModal);
+const mapStateToProps = state => ({
+    isConfirmed: state.preview.status.report === previewActions.Status.FETCHED,
+    isError: state.preview.status.report === previewActions.Status.ERROR,
+    isWaiting: state.preview.status.report === previewActions.Status.FETCHING
+});
+
+const mapDispatchToProps = () => ({});
+
+const ConnectedReportModal = connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(ReportModal);
+
+module.exports = injectIntl(ConnectedReportModal);
diff --git a/src/components/modal/report/modal.scss b/src/components/modal/report/modal.scss
index 6a9fe3abf..375ac6227 100644
--- a/src/components/modal/report/modal.scss
+++ b/src/components/modal/report/modal.scss
@@ -9,7 +9,7 @@
     margin: 100px auto;
     outline: none;
     padding: 0;
-    width: 30rem;
+    width: 36.25rem; /* 580px; */
     user-select: none;
 }
 
@@ -34,26 +34,45 @@
 .report-modal-content {
     margin: 1rem auto;
     width: 80%;
-    line-height: 1.5rem;
     font-size: .875rem;
 
+    .instructions {
+        line-height: 1.5rem;
+    }
+
+    .received {
+        margin: 0 auto;
+        width: 90%;
+        text-align: center;
+        line-height: 1.65rem;
+
+        .received-header {
+            font-weight: bold;
+        }
+    }
+
+    .error-text {
+        margin-top: .9375rem;
+    }
+
     .validation-message {
         $arrow-border-width: 1rem;
         display: block;
         position: absolute;
         top: 0;
-        left: 0;
-        transform: translate(23.5rem, 0);
+        left: 100%; /* position to the right of parent */
         margin-left: $arrow-border-width;
         border: 1px solid $active-gray;
         border-radius: 5px;
         background-color: $ui-orange;
         padding: 1rem;
+        min-width: 12rem;
         max-width: 18.75rem;
         min-height: 1rem;
         overflow: visible;
         color: $type-white;
 
+        /* arrow on box that points to the left */
         &:before {
             display: block;
             position: absolute;
@@ -78,3 +97,13 @@
 .report-modal-field {
     position: relative;
 }
+
+.form-group.has-error {
+    .textarea, select {
+        border: 1px solid $ui-orange;
+    }
+}
+
+.report-text .textarea {
+    margin-bottom: 0;
+}
diff --git a/src/l10n.json b/src/l10n.json
index 2c59f68de..a399ad6b2 100644
--- a/src/l10n.json
+++ b/src/l10n.json
@@ -172,6 +172,7 @@
     "registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
 
     "thumbnail.by": "by",
+    "report.error": "Something went wrong when trying to send your message. Please try again.",
     "report.project": "Report Project",
     "report.projectInstructions": "From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the {CommunityGuidelinesLink}.",
     "report.CommunityGuidelinesLinkText": "Scratch Community Guidelines",
@@ -181,8 +182,11 @@
     "report.reasonScary": "Too Violent or Scary",
     "report.reasonLanguage": "Inappropriate Language",
     "report.reasonMusic": "Inappropriate Music",
+    "report.reasonMissing": "Please select a reason",
     "report.reasonImage": "Inappropriate Images",
     "report.reasonPersonal": "Sharing Personal Contact Information",
+    "report.receivedHeader": "We have received your report!",
+    "report.receivedBody": "The Scratch Team will review the project based on the Scratch community guidelines.",
     "report.promptPlaceholder": "Select a reason why above.",
     "report.promptCopy": "Please provide a link to the original project",
     "report.promptUncredited": "Please provide links to the uncredited content",
@@ -194,5 +198,7 @@
     "report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image",
     "report.tooLongError": "That's too long! Please find a way to shorten your text.",
     "report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
-    "report.send": "Send"
+    "report.send": "Send",
+    "report.sending": "Sending...",
+    "report.textMissing": "Please tell us why you are reporting this project"
 }
diff --git a/src/redux/preview.js b/src/redux/preview.js
index 6ab017bf8..109635a12 100644
--- a/src/redux/preview.js
+++ b/src/redux/preview.js
@@ -1,3 +1,4 @@
+const defaults = require('lodash.defaults');
 const keyMirror = require('keymirror');
 const async = require('async');
 const merge = require('lodash.merge');
@@ -21,6 +22,7 @@ module.exports.getInitialState = () => ({
         original: module.exports.Status.NOT_FETCHED,
         parent: module.exports.Status.NOT_FETCHED,
         remixes: module.exports.Status.NOT_FETCHED,
+        report: module.exports.Status.NOT_FETCHED,
         projectStudios: module.exports.Status.NOT_FETCHED,
         curatedStudios: module.exports.Status.NOT_FETCHED,
         studioRequests: {}
@@ -324,6 +326,7 @@ module.exports.getReplies = (projectId, commentIds) => (dispatch => {
 });
 
 module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
+    dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
     if (faved) {
         api({
             uri: `/projects/${id}/favorites/user/${username}`,
@@ -383,6 +386,7 @@ module.exports.getLovedStatus = (id, username, token) => (dispatch => {
 });
 
 module.exports.setLovedStatus = (loved, id, username, token) => (dispatch => {
+    dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHING));
     if (loved) {
         api({
             uri: `/projects/${id}/loves/user/${username}`,
@@ -531,6 +535,7 @@ module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
 });
 
 module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
+    dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
     api({
         uri: `/projects/${id}`,
         authentication: token,
@@ -556,3 +561,27 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
         dispatch(module.exports.setProjectInfo(body));
     });
 });
+
+module.exports.reportProject = (id, jsonData) => (dispatch => {
+    dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHING));
+    // scratchr2 will fail if no thumbnail base64 string provided. We don't yet have
+    // a way to get the actual project thumbnail in www/gui, so for now just submit
+    // a minimal base64 png string.
+    defaults(jsonData, {
+        thumbnail: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC' +
+            '0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII='
+    });
+    api({
+        host: '',
+        uri: `/site-api/projects/all/${id}/report/`,
+        method: 'POST',
+        json: jsonData,
+        useCsrf: true
+    }, (err, body, res) => {
+        if (err || res.statusCode !== 200) {
+            dispatch(module.exports.setFetchStatus('report', module.exports.Status.ERROR));
+            return;
+        }
+        dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHED));
+    });
+});
diff --git a/src/views/preview/presentation.jsx b/src/views/preview/presentation.jsx
index b204eb831..a62aa663f 100644
--- a/src/views/preview/presentation.jsx
+++ b/src/views/preview/presentation.jsx
@@ -43,7 +43,7 @@ const PreviewPresentation = ({
     projectId,
     projectInfo,
     remixes,
-    report,
+    reportOpen,
     replies,
     addToStudioOpen,
     projectStudios,
@@ -278,8 +278,8 @@ const PreviewPresentation = ({
                                                 Report
                                             </Button>,
                                             <ReportModal
+                                                isOpen={reportOpen}
                                                 key="report-modal"
-                                                report={report}
                                                 type="project"
                                                 onReport={onReportSubmit}
                                                 onRequestClose={onReportClose}
@@ -377,12 +377,7 @@ PreviewPresentation.propTypes = {
     projectStudios: PropTypes.arrayOf(PropTypes.object),
     remixes: PropTypes.arrayOf(PropTypes.object),
     replies: PropTypes.objectOf(PropTypes.array),
-    report: PropTypes.shape({
-        category: PropTypes.string,
-        notes: PropTypes.string,
-        open: PropTypes.bool,
-        waiting: PropTypes.bool
-    }),
+    reportOpen: PropTypes.bool,
     studios: PropTypes.arrayOf(PropTypes.object),
     userOwnsProject: PropTypes.bool
 };
diff --git a/src/views/preview/preview.jsx b/src/views/preview/preview.jsx
index 128e30854..37f20e7b3 100644
--- a/src/views/preview/preview.jsx
+++ b/src/views/preview/preview.jsx
@@ -57,12 +57,7 @@ class Preview extends React.Component {
             loveCount: 0,
             projectId: parts[1] === 'editor' ? 0 : parts[1],
             addToStudioOpen: false,
-            report: {
-                category: '',
-                notes: '',
-                open: false,
-                waiting: false
-            }
+            reportOpen: false
         };
         this.getExtensions(this.state.projectId);
         this.addEventListeners();
@@ -87,7 +82,6 @@ class Preview extends React.Component {
                 this.props.getRemixes(this.state.projectId);
                 this.props.getProjectStudios(this.state.projectId);
             }
-
         }
         if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
             this.getExtensions(this.state.projectId);
@@ -118,7 +112,7 @@ class Preview extends React.Component {
     getExtensions (projectId) {
         storage
             .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON)
-            .then(projectAsset => {
+            .then(projectAsset => { // NOTE: this is turning up null, breaking the line below.
                 let input = projectAsset.data;
                 if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
                 !ArrayBuffer.isView(input)) { // taken from scratch-vm
@@ -148,10 +142,10 @@ class Preview extends React.Component {
             });
     }
     handleReportClick () {
-        this.setState({report: {...this.state.report, open: true}});
+        this.setState({reportOpen: true});
     }
     handleReportClose () {
-        this.setState({report: {...this.state.report, open: false}});
+        this.setState({reportOpen: false});
     }
     handleAddToStudioClick () {
         this.setState({addToStudioOpen: true});
@@ -159,27 +153,8 @@ class Preview extends React.Component {
     handleAddToStudioClose () {
         this.setState({addToStudioOpen: false});
     }
-    // NOTE: this is a copy, change it
     handleReportSubmit (formData) {
-        this.setState({report: {
-            category: formData.report_category,
-            notes: formData.notes,
-            open: this.state.report.open,
-            waiting: true}
-        });
-
-        const data = {
-            ...formData,
-            id: this.state.projectId,
-            user: this.props.user.username
-        };
-        console.log('submit report data', data); // eslint-disable-line no-console
-        this.setState({report: {
-            category: '',
-            notes: '',
-            open: false,
-            waiting: false}
-        });
+        this.props.reportProject(this.state.projectId, formData);
     }
     handlePopState () {
         const path = window.location.pathname.toLowerCase();
@@ -335,7 +310,7 @@ class Preview extends React.Component {
                         projectStudios={this.props.projectStudios}
                         remixes={this.props.remixes}
                         replies={this.props.replies}
-                        report={this.state.report}
+                        reportOpen={this.state.reportOpen}
                         studios={this.props.studios}
                         user={this.props.user}
                         userOwnsProject={this.userOwnsProject()}
@@ -383,6 +358,7 @@ Preview.propTypes = {
     projectStudios: PropTypes.arrayOf(PropTypes.object),
     remixes: PropTypes.arrayOf(PropTypes.object),
     replies: PropTypes.objectOf(PropTypes.array),
+    reportProject: PropTypes.func,
     sessionStatus: PropTypes.string,
     setFavedStatus: PropTypes.func.isRequired,
     setFullScreen: PropTypes.func.isRequired,
@@ -461,7 +437,6 @@ const mapStateToProps = state => ({
     fullScreen: state.scratchGui.mode.isFullScreen
 });
 
-
 const mapDispatchToProps = dispatch => ({
     getOriginalInfo: id => {
         dispatch(previewActions.getOriginalInfo(id));
@@ -506,6 +481,9 @@ const mapDispatchToProps = dispatch => ({
     refreshSession: () => {
         dispatch(sessionActions.refreshSession());
     },
+    reportProject: (id, formData) => {
+        dispatch(previewActions.reportProject(id, formData));
+    },
     setOriginalInfo: info => {
         dispatch(previewActions.setOriginalInfo(info));
     },