Report first pass

Report button opens a modal with the report project form. All the content is currently in `preview/presentation` because components cannot have their own l10n files.
This commit is contained in:
chrisgarrity 2018-05-24 13:47:31 -04:00
parent bd391b422a
commit 34042c9e8e
6 changed files with 568 additions and 336 deletions

View file

@ -18,8 +18,9 @@ $ui-border: hsla(0, 0, 85, 1); //#D9D9D9
/* 3.0 colors */
/* Using www naming convention for now, should be consistent with gui */
$ui-green: hsla(163, 83, 40, 1); //#0fbd8c Pen Primary
$ui-coral: hsla(350, 100, 70, 1); //#FF6680 More Priamry
$ui-green: hsla(163, 83, 40, 1); // #0fbd8c Pen Primary
$ui-coral: hsla(350, 100, 70, 1); // #FF6680 More Primary
$ui-coral-dark: hsla(350, 100, 60, 1); // #FF3355 More tertiary
$ui-blue-10percent: hsla(215, 100, 65, .1);
$ui-orange-25percent: hsla(35, 90, 55, .25);

View file

@ -0,0 +1,34 @@
const PropTypes = require('prop-types');
const React = require('react');
const Modal = require('../base/modal.jsx');
require('../../forms/button.scss');
require('./modal.scss');
const ReportModal = props => (
<Modal
className="mod-report"
{...props}
>
<div>
<div className="report-modal-header">
<div className="report-content-label">
{props.contentLabel}
</div>
</div>
<div className="report-modal-content">
{props.children}
</div>
</div>
</Modal>
);
ReportModal.propTypes = {
children: PropTypes.node,
contentLabel: PropTypes.string,
onRequestClose: PropTypes.func
};
module.exports = ReportModal;

View file

@ -0,0 +1,41 @@
@import "../../../colors";
@import "../../../frameless";
.mod-report * {
box-sizing: border-box;
}
.mod-report {
margin: 100px auto;
width: 30rem;
outline: none;
padding: 0;
user-select: none;
overflow: hidden;
}
.report-modal-header {
height: 3rem;
padding-top: .75rem;
box-sizing: border-box;
width: 100%;
background-color: $ui-coral;
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
}
.report-content-label {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: bold;
color: $type-white;
text-align: center;
}
.report-modal-content {
width: 80%;
margin: 1rem auto;
font-size: .875rem;
line-height: 1.5rem;
}

View file

@ -1,3 +1,4 @@
const bindAll = require('lodash.bindall');
const FormattedDate = require('react-intl').FormattedDate;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
@ -13,329 +14,499 @@ const sessionActions = require('../../redux/session.js');
const decorateText = require('../../lib/decorate-text.jsx');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx');
const Form = require('../../components/forms/form.jsx');
const Input = require('../../components/forms/input.jsx');
const Select = require('../../components/forms/select.jsx');
const Spinner = require('../../components/spinner/spinner.jsx');
const TextArea = require('../../components/forms/textarea.jsx');
const Avatar = require('../../components/avatar/avatar.jsx');
const CappedNumber = require('../../components/cappednumber/cappednumber.jsx');
const ShareBanner = require('../../components/share-banner/share-banner.jsx');
const ThumbnailColumn = require('../../components/thumbnailcolumn/thumbnailcolumn.jsx');
const InplaceInput = require('../../components/forms/inplace-input.jsx');
const ReportModal = require('../../components/modal/report/modal.jsx');
require('./preview.scss');
const PreviewPresentation = props => {
const {
editable,
faved,
favoriteCount,
intl,
isFullScreen,
loved,
loveCount,
originalInfo,
parentInfo,
projectId,
projectInfo,
remixes,
sessionStatus,
studios,
user,
onFavoriteClicked,
onLoveClicked,
onSeeInside,
onUpdate
// ...otherProps TBD
} = props;
const shareDate = (projectInfo.history && projectInfo.history.shared) ? projectInfo.history.shared : '';
return (
<div className="preview">
{projectInfo.history && shareDate === '' &&
<ShareBanner>
<FlexRow className="preview-row">
<span className="share-text">
This project is not shared so only you can see it. Click share to let everyone see it!
</span>
<Button className="button share-button">
Share
</Button>
</FlexRow>
</ShareBanner>
}
{ projectInfo && projectInfo.author && projectInfo.author.id && (
<div className="inner">
<Formsy>
class PreviewPresentation extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleReportClick',
'handleReportClose',
'handleReportReasonSelect',
'handleReportSubmit'
]);
this.state = {
reportOpen: false,
reportPrompt: 'Select a reason why above.',
reportReason: ''
};
}
handleReportClick (e) {
e.preventDefault();
this.setState({reportOpen: true});
}
handleReportClose () {
this.setState({reportOpen: false});
}
handleReportReasonSelect (name, value) {
const prompts = [
'Please provide a link to the original project',
'Please provide links to the uncredited content',
'Please say why the project is too violent or scary',
'Please say where the inappropriate language occurs in the project (For example: Notes & Credits, sprite name, project text, etc.)',
'Please say the name of the audio file with the inappropriate music',
'Please say where the personal contact information is shared (For example: Notes & Credits, sprite name, project text, etc.)',
'Please be specific about why this project does not follow our Community Guidelines',
'not used',
'Please say the name of the sprite or the backdrop with the inappropriate image'
];
this.setState({reportPrompt: prompts[value]});
}
handleReportSubmit (formData) {
console.log('submit report data', formData);
this.setState({reportOpen: false});
}
render () {
const {
editable,
faved,
favoriteCount,
intl,
isFullScreen,
loved,
loveCount,
originalInfo,
parentInfo,
projectId,
projectInfo,
remixes,
sessionStatus,
studios,
user,
onFavoriteClicked,
onLoveClicked,
onSeeInside,
onUpdate
// ...otherProps TBD
} = this.props;
const shareDate = (projectInfo.history && projectInfo.history.shared) ? projectInfo.history.shared : '';
return (
<div className="preview">
{projectInfo.history && shareDate === '' &&
<ShareBanner>
<FlexRow className="preview-row">
<FlexRow className="project-header">
<Avatar
src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo. author.id}_48x48.png`}
/>
<div className="title">
{editable ?
<InplaceInput
className="project-title"
handleUpdate={onUpdate}
name="title"
validationErrors={{
maxLength: 'Sorry title is too long'
// maxLength: props.intl.formatMessage({
// id: 'project.titleMaxLength'
// })
}}
validations={{
// TODO: actual 100
maxLength: 40
}}
value={projectInfo.title}
/> :
<div className="project-title">{projectInfo.title}</div>
<span className="share-text">
This project is not shared so only you can see it. Click share to let everyone see it!
</span>
<Button className="button share-button">
Share
</Button>
</FlexRow>
</ShareBanner>
}
{ projectInfo && projectInfo.author && projectInfo.author.id && (
<div className="inner">
<Formsy>
<FlexRow className="preview-row">
<FlexRow className="project-header">
<Avatar
src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo. author.id}_48x48.png`}
/>
<div className="title">
{editable ?
<InplaceInput
className="project-title"
handleUpdate={onUpdate}
name="title"
validationErrors={{
maxLength: 'Sorry title is too long'
// maxLength: props.intl.formatMessage({
// id: 'project.titleMaxLength'
// })
}}
validations={{
// TODO: actual 100
maxLength: 40
}}
value={projectInfo.title}
/> :
<div className="project-title">{projectInfo.title}</div>
}
{`${intl.formatMessage({id: 'thumbnail.by'})} `}
<a href={`/users/${projectInfo.author.username}`}>
{projectInfo.author.username}
</a>
</div>
</FlexRow>
<div className="project-buttons">
{sessionStatus === sessionActions.Status.FETCHED &&
Object.keys(user).length > 0 &&
user.id !== projectInfo.author.id &&
<Button className="button remix-button">
Remix
</Button>
}
{`${intl.formatMessage({id: 'thumbnail.by'})} `}
<a href={`/users/${projectInfo.author.username}`}>
{projectInfo.author.username}
</a>
</div>
</FlexRow>
<div className="project-buttons">
{sessionStatus === sessionActions.Status.FETCHED &&
Object.keys(user).length > 0 &&
user.id !== projectInfo.author.id &&
<Button className="button remix-button">
Remix
<Button
className="button see-inside-button"
onClick={onSeeInside}
>
See Inside
</Button>
}
<Button
className="button see-inside-button"
onClick={onSeeInside}
>
See Inside
</Button>
</div>
</FlexRow>
<FlexRow className="preview-row">
<div className="guiPlayer">
<IntlGUI
isPlayerOnly
basePath="/"
className="guiPlayer"
isFullScreen={isFullScreen}
previewInfoVisible="false"
projectId={projectId}
/>
</div>
<FlexRow className="project-notes">
{parentInfo && parentInfo.author && parentInfo.id && (
<FlexRow className="remix-credit">
<Avatar
className="remix"
src={`https://cdn2.scratch.mit.edu/get_image/user/${parentInfo.author.id}_48x48.png`}
/>
<div className="credit-text">
Thanks to <a
href={`/users/${parentInfo.author.username}`}
>
{parentInfo.author.username}
</a> for the original project <a
href={`/preview/${parentInfo.id}`}
>
{parentInfo.title}
</a>.
</div>
</FlexRow>
)}
{originalInfo && originalInfo.author && originalInfo.id && (
<FlexRow className="remix-credit">
<Avatar
className="remix"
src={`https://cdn2.scratch.mit.edu/get_image/user/${originalInfo.author.id}_48x48.png`}
/>
<div className="credit-text">
Thanks to <a
href={`/users/${originalInfo.author.username}`}
>
{originalInfo.author.username}
</a> for the original project <a
href={`/preview/${originalInfo.id}`}
>
{originalInfo.title}
</a>.
</div>
</FlexRow>
)}
<FlexRow className="description-block">
<div className="project-textlabel">
Instructions
</div>
{editable ?
<InplaceInput
className={classNames(
'project-description-edit',
{remixes: parentInfo && parentInfo.author}
)}
handleUpdate={onUpdate}
name="instructions"
type="textarea"
validationErrors={{
maxLength: 'Sorry description is too long'
// maxLength: props.intl.formatMessage({
// id: 'project.descriptionMaxLength'
// })
}}
validations={{
// TODO: actual 5000
maxLength: 1000
}}
value={projectInfo.instructions}
/> :
<div className="project-description">
{decorateText(projectInfo.instructions)}
</div>
}
</FlexRow>
<FlexRow className="description-block">
<div className="project-textlabel">
Notes and Credits
</div>
{editable ?
<InplaceInput
className={classNames(
'project-description-edit',
{remixes: parentInfo && parentInfo.author}
)}
handleUpdate={onUpdate}
name="description"
type="textarea"
validationErrors={{
maxLength: 'Sorry description is too long'
// maxLength: props.intl.formatMessage({
// id: 'project.descriptionMaxLength'
// })
}}
validations={{
// TODO: actual 5000
maxLength: 1000
}}
value={projectInfo.description}
/> :
<div className="project-description last">
{decorateText(projectInfo.description)}
</div>
}
</FlexRow>
</FlexRow>
</FlexRow>
<FlexRow className="preview-row">
<FlexRow className="stats">
<div
className={classNames('project-loves', {loved: loved})}
key="loves"
onClick={onLoveClicked}
>
{loveCount}
</div>
<div
className={classNames('project-favorites', {favorited: faved})}
key="favorites"
onClick={onFavoriteClicked}
>
{favoriteCount}
</div>
<div
className="project-remixes"
key="remixes"
>
{projectInfo.remix.count}
</div>
<div
className="project-views"
key="views"
>
<CappedNumber value={projectInfo.stats.views} />
</div>
</FlexRow>
<FlexRow className="subactions">
<div className="share-date">
<div className="copyleft">&copy;</div>
{' '}
{/* eslint-disable react/jsx-sort-props */}
{shareDate === null ?
'Unshared' :
<FormattedDate
value={Date.parse(shareDate)}
day="2-digit"
month="short"
year="numeric"
/>
}
{/* eslint-enable react/jsx-sort-props */}
<FlexRow className="preview-row">
<div className="guiPlayer">
<IntlGUI
isPlayerOnly
basePath="/"
className="guiPlayer"
isFullScreen={isFullScreen}
previewInfoVisible="false"
projectId={projectId}
/>
</div>
<FlexRow className="project-notes">
{parentInfo && parentInfo.author && parentInfo.id && (
<FlexRow className="remix-credit">
<Avatar
className="remix"
src={`https://cdn2.scratch.mit.edu/get_image/user/${parentInfo.author.id}_48x48.png`}
/>
<div className="credit-text">
Thanks to <a
href={`/users/${parentInfo.author.username}`}
>
{parentInfo.author.username}
</a> for the original project <a
href={`/preview/${parentInfo.id}`}
>
{parentInfo.title}
</a>.
</div>
</FlexRow>
)}
{originalInfo && originalInfo.author && originalInfo.id && (
<FlexRow className="remix-credit">
<Avatar
className="remix"
src={`https://cdn2.scratch.mit.edu/get_image/user/${originalInfo.author.id}_48x48.png`}
/>
<div className="credit-text">
Thanks to <a
href={`/users/${originalInfo.author.username}`}
>
{originalInfo.author.username}
</a> for the original project <a
href={`/preview/${originalInfo.id}`}
>
{originalInfo.title}
</a>.
</div>
</FlexRow>
)}
<FlexRow className="description-block">
<div className="project-textlabel">
Instructions
</div>
{editable ?
<InplaceInput
className={classNames(
'project-description-edit',
{remixes: parentInfo && parentInfo.author}
)}
handleUpdate={onUpdate}
name="instructions"
type="textarea"
validationErrors={{
maxLength: 'Sorry description is too long'
// maxLength: props.intl.formatMessage({
// id: 'project.descriptionMaxLength'
// })
}}
validations={{
// TODO: actual 5000
maxLength: 1000
}}
value={projectInfo.instructions}
/> :
<div className="project-description">
{decorateText(projectInfo.instructions)}
</div>
}
</FlexRow>
<FlexRow className="description-block">
<div className="project-textlabel">
Notes and Credits
</div>
{editable ?
<InplaceInput
className={classNames(
'project-description-edit',
{remixes: parentInfo && parentInfo.author}
)}
handleUpdate={onUpdate}
name="description"
type="textarea"
validationErrors={{
maxLength: 'Sorry description is too long'
// maxLength: props.intl.formatMessage({
// id: 'project.descriptionMaxLength'
// })
}}
validations={{
// TODO: actual 5000
maxLength: 1000
}}
value={projectInfo.description}
/> :
<div className="project-description last">
{decorateText(projectInfo.description)}
</div>
}
</FlexRow>
</FlexRow>
</FlexRow>
<FlexRow className="preview-row">
<FlexRow className="stats">
<div
className={classNames('project-loves', {loved: loved})}
key="loves"
onClick={onLoveClicked}
>
{loveCount}
</div>
<div
className={classNames('project-favorites', {favorited: faved})}
key="favorites"
onClick={onFavoriteClicked}
>
{favoriteCount}
</div>
<div
className="project-remixes"
key="remixes"
>
{projectInfo.remix.count}
</div>
<div
className="project-views"
key="views"
>
<CappedNumber value={projectInfo.stats.views} />
</div>
</FlexRow>
<FlexRow className="subactions">
<div className="share-date">
<div className="copyleft">&copy;</div>
{' '}
{/* eslint-disable react/jsx-sort-props */}
{shareDate === null ?
'Unshared' :
<FormattedDate
value={Date.parse(shareDate)}
day="2-digit"
month="short"
year="numeric"
/>
}
{/* eslint-enable react/jsx-sort-props */}
</div>
<FlexRow className="action-buttons">
<Button className="action-button studio-button">
Add to Studio
</Button>
<Button className="action-button copy-link-button">
Copy Link
</Button>
<Button className="action-button report-button">
Report
</Button>
<FlexRow className="action-buttons">
<Button className="action-button studio-button">
Add to Studio
</Button>
<Button className="action-button copy-link-button">
Copy Link
</Button>
{
sessionStatus === sessionActions.Status.FETCHED &&
Object.keys(user).length > 0 &&
user.id !== projectInfo.author.id && [
<Button
className="action-button report-button"
key="report-button"
onClick={this.handleReportClick}
>
Report
</Button>,
<ReportModal
contentLabel="Report Project"
isOpen={this.state.reportOpen}
key="report-modal"
onRequestClose={this.handleReportClose}
onSubmit={this.handleReportSubmit}
>
From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the Scratch Community Guidelines.
<Form
className="report"
onSubmit={this.handleReportSubmit}
>
<Input
name="projectId"
type="hidden"
value={projectId}
/>
<Input
name="username"
type="hidden"
value={user.username}
/>
<Select
required
name="reason"
options={[
{
value: '',
label: 'Select a reason'
},
{
value: '0',
label: 'Exact Copy of Project'
},
{
value: '1',
label: 'Uses Image/Music Without Credit'
},
{
value: '2',
label: 'Too Violent or Scary'
},
{
value: '3',
label: 'Inappropriate Language'
},
{
value: '4',
label: 'Inappropriate Music'
},
{
value: '8',
label: 'Inappropriate Images'
},
{
value: '5',
label: 'Sharing Personal Contact Information'
},
{
value: '6',
label: 'Other'
}
]}
value={this.state.reportReason}
onChange={this.handleReportReasonSelect}
/>
<TextArea
required
className="report-text"
name="reportText"
placeholder={this.state.reportPrompt}
validationErrors={{
maxLength: 'That\'s too long! Please find a way to shorten your text.',
minLength: 'That\'s too short. Please describe in detail what\'s inappropriate or disrespectful about the project.'
}}
validations={{
// TODO find out max and min characters
maxLength: 500,
minLength: 30
}}
/>
{this.state.reportWaiting ? [
<Button
className="submit-button white"
disabled="disabled"
key="submitButton"
type="submit"
>
<Spinner />
</Button>
] : [
<Button
className="submit-button white"
key="submitButton"
type="submit"
>
Send
</Button>
]}
</Form>
</ReportModal>
]
}
</FlexRow>
</FlexRow>
</FlexRow>
</FlexRow>
<FlexRow className="preview-row">
<div className="comments-container">
<div className="project-title">
Comments go here
<FlexRow className="preview-row">
<div className="comments-container">
<div className="project-title">
Comments go here
</div>
</div>
</div>
<FlexRow className="column">
<FlexRow className="remix-list">
<div className="project-title">
Remixes
</div>
{remixes && remixes.length === 0 ? (
<span>No remixes</span>
) : (
<ThumbnailColumn
cards
showAvatar
itemType="preview"
items={remixes.slice(0, 5)}
showFavorites={false}
showLoves={false}
showViews={false}
/>
<FlexRow className="column">
{/* hide remixes if there aren't any */}
{remixes && remixes.length !== 0 && (
<FlexRow className="remix-list">
<div className="project-title">
Remixes
</div>
{remixes && remixes.length === 0 ? (
// TODO: style remix invitation
<span>Invite user to remix</span>
) : (
<ThumbnailColumn
cards
showAvatar
itemType="preview"
items={remixes.slice(0, 5)}
showFavorites={false}
showLoves={false}
showViews={false}
/>
)}
</FlexRow>
)}
</FlexRow>
<FlexRow className="studio-list">
<div className="project-title">
Studios
</div>
{studios && studios.length === 0 ? (
<span>No studios</span>
) : (
<ThumbnailColumn
cards
showAvatar
itemType="gallery"
items={studios.slice(0, 5)}
showFavorites={false}
showLoves={false}
showViews={false}
/>
{/* hide studios if there aren't any */}
{studios && studios.length !== 0 && (
<FlexRow className="studio-list">
<div className="project-title">
Studios
</div>
{studios && studios.length === 0 ? (
// TODO: invite user to add to studio?
<span>None </span>
) : (
<ThumbnailColumn
cards
showAvatar
itemType="gallery"
items={studios.slice(0, 5)}
showFavorites={false}
showLoves={false}
showViews={false}
/>
)}
</FlexRow>
)}
</FlexRow>
</FlexRow>
</FlexRow>
</Formsy>
</div>
)}
</div>
);
};
</Formsy>
</div>
)}
</div>
);
}
}
PreviewPresentation.propTypes = {
editable: PropTypes.bool,
@ -389,17 +560,6 @@ PreviewPresentation.propTypes = {
root: PropTypes.number
})
}),
editable: PropTypes.bool,
faved: PropTypes.bool,
favoriteCount: PropTypes.number,
intl: intlShape,
isFullScreen: PropTypes.bool,
loveCount: PropTypes.number,
loved: PropTypes.bool,
onFavoriteClicked: PropTypes.func,
onLoveClicked: PropTypes.func,
onSeeInside: PropTypes.func,
onUpdate: PropTypes.func,
projectId: PropTypes.string,
projectInfo: PropTypes.shape({
id: PropTypes.number,

View file

@ -69,6 +69,19 @@ class Preview extends React.Component {
componentWillUnmount () {
this.removeEventListeners();
}
initState () {
const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean);
// parts[0]: 'preview'
// parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
return {
editable: false,
favoriteCount: 0,
loveCount: 0,
projectId: parts[1] === 'editor' ? 0 : parts[1]
};
}
addEventListeners () {
window.addEventListener('popstate', this.handlePopState);
}
@ -108,19 +121,6 @@ class Preview extends React.Component {
);
}
}
initState () {
const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean);
// parts[0]: 'preview'
// parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
return {
editable: false,
favoriteCount: 0,
loveCount: 0,
projectId: parts[1] === 'editor' ? 0 : parts[1]
};
}
handleFavoriteToggle () {
this.props.setFavedStatus(
!this.props.faved,
@ -269,15 +269,6 @@ Preview.propTypes = {
root: PropTypes.number
})
}),
faved: PropTypes.bool,
fullScreen: PropTypes.bool,
getCreditInfo: PropTypes.func.isRequired,
getFavedStatus: PropTypes.func.isRequired,
getLovedStatus: PropTypes.func.isRequired,
getProjectInfo: PropTypes.func.isRequired,
getRemixes: PropTypes.func.isRequired,
getStudios: PropTypes.func.isRequired,
loved: PropTypes.bool,
playerMode: PropTypes.bool,
projectInfo: PropTypes.shape({
author: PropTypes.shape({

View file

@ -462,3 +462,8 @@ $stage-height: 406px;
}
}
}
.report-text textarea {
// override min-height from default settings (for teacher registration)
min-height: 8rem;
}