Merge pull request #1912 from chrisgarrity/feature/preview-restructure

Feature/preview restructure
This commit is contained in:
Ray Schamp 2018-06-19 10:47:40 -04:00 committed by GitHub
commit 5740fe8bf8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 651 additions and 541 deletions

View file

@ -29,8 +29,7 @@
"express-http-proxy": "1.1.0",
"lodash.defaults": "4.0.1",
"newrelic": "1.25.4",
"raven": "0.10.0",
"scratch-gui": "0.1.0-prerelease.20180529181946"
"raven": "0.10.0"
},
"devDependencies": {
"ajv": "6.4.0",
@ -97,6 +96,7 @@
"redux-thunk": "2.0.1",
"sass-lint": "1.5.1",
"sass-loader": "6.0.6",
"scratch-gui": "0.1.0-prerelease.20180605163331",
"scratchr2_translations": "git://github.com/LLK/scratchr2_translations.git#master",
"slick-carousel": "1.6.0",
"source-map-support": "0.3.2",

View file

@ -38,6 +38,8 @@ class InplaceInput extends React.Component {
<FRCTextarea
className="inplace-textarea"
componentRef={this.setRef}
elementWrapperClassName="grow"
label={null}
rowClassName={classNames('textarea-row no-label', className)}
onBlur={this.handleBlur}
{...props}

View file

@ -5,7 +5,6 @@ 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 log = require('../../../lib/log.js');
const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx');
@ -16,48 +15,74 @@ const TextArea = require('../../forms/textarea.jsx');
require('../../forms/button.scss');
require('./modal.scss');
const REPORT_OPTIONS = [
{
value: '',
label: {id: 'report.reasonPlaceHolder'},
prompt: {id: 'report.promptPlaceholder'}
},
{
value: '0',
label: {id: 'report.reasonCopy'},
prompt: {id: 'report.promptCopy'}
},
{
value: '1',
label: {id: 'report.reasonUncredited'},
prompt: {id: 'report.promptUncredited'}
},
{
value: '2',
label: {id: 'report.reasonScary'},
prompt: {id: 'report.promptScary'}
},
{
value: '3',
label: {id: 'report.reasonLanguage'},
prompt: {id: 'report.promptLanguage'}
},
{
value: '4',
label: {id: 'report.reasonMusic'},
prompt: {id: 'report.promptMusic'}
},
{
value: '8',
label: {id: 'report.reasonImage'},
prompt: {id: 'report.promptImage'}
},
{
value: '5',
label: {id: 'report.reasonPersonal'},
prompt: {id: 'report.promptPersonal'}
},
{
value: '6',
label: {id: 'general.other'},
prompt: {id: 'report.promptGuidelines'}
}
];
class ReportModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleReasonSelect',
'handleSubmit'
'handleReportCategorySelect'
]);
this.state = {
prompt: props.intl.formatMessage({id: 'report.promptPlaceholder'}),
reason: '',
waiting: false
};
this.state = {reportCategory: this.props.report.category};
}
handleReasonSelect (name, value) {
const prompts = [
this.props.intl.formatMessage({id: 'report.promptCopy'}),
this.props.intl.formatMessage({id: 'report.promptUncredited'}),
this.props.intl.formatMessage({id: 'report.promptScary'}),
this.props.intl.formatMessage({id: 'report.promptLanguage'}),
this.props.intl.formatMessage({id: 'report.promptMusic'}),
this.props.intl.formatMessage({id: 'report.promptPersonal'}),
this.props.intl.formatMessage({id: 'report.promptGuidelines'}),
'not used',
this.props.intl.formatMessage({id: 'report.promptImage'})
];
this.setState({prompt: prompts[value], reason: value});
handleReportCategorySelect (name, value) {
this.setState({reportCategory: value});
}
handleSubmit (formData) {
this.setState({waiting: true});
this.props.onReport(formData, err => {
if (err) log.error(err);
this.setState({
prompt: this.props.intl.formatMessage({id: 'report.promptPlaceholder'}),
reason: '',
waiting: false
});
});
lookupPrompt (value) {
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
return this.props.intl.formatMessage(prompt);
}
render () {
const {
intl,
onReport, // eslint-disable-line no-unused-vars
report,
type,
...modalProps
} = this.props;
@ -66,6 +91,7 @@ class ReportModal extends React.Component {
<Modal
className="mod-report"
contentLabel={contentLabel}
isOpen={report.open}
{...modalProps}
>
<div>
@ -88,68 +114,38 @@ class ReportModal extends React.Component {
/>
<Form
className="report"
onSubmit={this.handleSubmit}
onSubmit={onReport}
>
<Select
required
name="reason"
options={[
{
value: '',
label: this.props.intl.formatMessage({id: 'report.reasonPlaceHolder'})
},
{
value: '0',
label: this.props.intl.formatMessage({id: 'report.reasonCopy'})
},
{
value: '1',
label: this.props.intl.formatMessage({id: 'report.reasonUncredited'})
},
{
value: '2',
label: this.props.intl.formatMessage({id: 'report.reasonScary'})
},
{
value: '3',
label: this.props.intl.formatMessage({id: 'report.reasonLanguage'})
},
{
value: '4',
label: this.props.intl.formatMessage({id: 'report.reasonMusic'})
},
{
value: '8',
label: this.props.intl.formatMessage({id: 'report.reasonImage'})
},
{
value: '5',
label: this.props.intl.formatMessage({id: 'report.reasonPersonal'})
},
{
value: '6',
label: this.props.intl.formatMessage({id: 'general.other'})
}
]}
value={this.state.reason}
onChange={this.handleReasonSelect}
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"
name="reportText"
placeholder={this.state.prompt}
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={{
// TODO find out max and min characters
maxLength: 500,
minLength: 30
minLength: 20
}}
value={report.notes}
/>
{this.state.reportWaiting ? [
{report.waiting ? [
<Button
className="submit-button white"
disabled="disabled"
@ -180,6 +176,12 @@ ReportModal.propTypes = {
intl: intlShape,
onReport: PropTypes.func,
onRequestClose: PropTypes.func,
report: PropTypes.shape({
category: PropTypes.string,
notes: PropTypes.string,
open: PropTypes.bool,
waiting: PropTypes.bool
}),
type: PropTypes.string
};

View file

@ -10,11 +10,11 @@
outline: none;
padding: 0;
width: 30rem;
overflow: hidden;
user-select: none;
}
.report-modal-header {
border-radius: 1rem 1rem 0 0;
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
background-color: $ui-coral;
padding-top: .75rem;
@ -36,4 +36,45 @@
width: 80%;
line-height: 1.5rem;
font-size: .875rem;
.validation-message {
$arrow-border-width: 1rem;
display: block;
position: absolute;
top: 0;
left: 0;
transform: translate(23.5rem, 0);
margin-left: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
max-width: 18.75rem;
min-height: 1rem;
overflow: visible;
color: $type-white;
&:before {
display: block;
position: absolute;
top: 1rem;
left: -$arrow-border-width / 2;
transform: rotate(45deg);
border-bottom: 1px solid $active-gray;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
width: $arrow-border-width;
height: $arrow-border-width;
content: "";
}
}
}
.report-modal-field {
position: relative;
}

View file

@ -1,20 +0,0 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
require('./share-banner.scss');
const ShareBanner = props => (
<div className={classNames('shareBanner', props.className)}>
<div className="inner">
{props.children}
</div>
</div>
);
ShareBanner.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
module.exports = ShareBanner;

View file

@ -1,10 +0,0 @@
@import "../../colors";
$navigation-height: 50px;
.shareBanner {
background-color: $ui-orange-25percent;
width: 100%;
overflow: hidden;
color: $ui-orange;
}

View file

@ -14,7 +14,7 @@ const ThumbnailColumn = props => (
if (props.itemType === 'preview') {
return (
<Thumbnail
avatar={`https://cdn2.scratch.mit.edu/get_image/user/${item.author.i}_32x32.png`}
avatar={`https://cdn2.scratch.mit.edu/get_image/user/${item.author.id}_32x32.png`}
creator={item.author.username}
favorites={item.stats.favorites}
href={href}

View file

@ -1,4 +1,3 @@
const bindAll = require('lodash.bindall');
const FormattedDate = require('react-intl').FormattedDate;
const injectIntl = require('react-intl').injectIntl;
const PropTypes = require('prop-types');
@ -10,401 +9,289 @@ const approx = require('approximate-number');
const GUI = require('scratch-gui').default;
const IntlGUI = injectIntl(GUI);
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 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 ShareBanner = require('./share-banner.jsx');
const RemixCredit = require('./remix-credit.jsx');
const RemixList = require('./remix-list.jsx');
const StudioList = require('./studio-list.jsx');
const InplaceInput = require('../../components/forms/inplace-input.jsx');
const ReportModal = require('../../components/modal/report/modal.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
require('./preview.scss');
class PreviewPresentation extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleReportClick',
'handleReportClose',
'handleReportSubmit'
]);
this.state = {
reportOpen: false
};
}
handleReportClick (e) {
e.preventDefault();
this.setState({reportOpen: true});
}
handleReportClose () {
this.setState({reportOpen: false});
}
handleReportSubmit (formData, callback) {
const data = {
...formData,
id: this.props.projectId,
username: this.props.user.username
};
console.log('submit report data', data); // eslint-disable-line no-console
// TODO: pass error to modal via callback.
callback();
this.setState({reportOpen: false});
}
render () {
const {
editable,
faved,
favoriteCount,
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">
<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">
<a href={`/users/${projectInfo.author.username}`}>
<Avatar
alt={projectInfo.author.username}
src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo. author.id}_48x48.png`}
/>
</a>
<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={{
maxLength: 100
}}
value={projectInfo.title}
/> :
<div className="project-title">{projectInfo.title}</div>
}
</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>
}
<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>
)}
{/* eslint-disable max-len */}
<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"
placeholder="Tell people how to use your project (such as which keys to press)."
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',
'last',
{remixes: parentInfo && parentInfo.author}
)}
handleUpdate={onUpdate}
name="description"
placeholder="How did you make this project? Did you use ideas scripts or artwork from other people? Thank them here."
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>
{/* eslint-enable max-len */}
</FlexRow>
</FlexRow>
<FlexRow className="preview-row">
<FlexRow className="stats">
<div
className={classNames('project-loves', {loved: loved})}
key="loves"
onClick={onLoveClicked}
>
{approx(loveCount, {decimal: false})}
</div>
<div
className={classNames('project-favorites', {favorited: faved})}
key="favorites"
onClick={onFavoriteClicked}
>
{approx(favoriteCount, {decimal: false})}
</div>
<div
className="project-remixes"
key="remixes"
>
{approx(projectInfo.stats.remixes, {decimal: false})}
</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>
{
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
isOpen={this.state.reportOpen}
key="report-modal"
type="project"
onReport={this.handleReportSubmit}
onRequestClose={this.handleReportClose}
/>
]
}
</FlexRow>
</FlexRow>
</FlexRow>
<FlexRow className="preview-row">
<div className="comments-container">
<div className="project-title">
Comments go here
</div>
</div>
<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>
)}
{/* 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>
</Formsy>
</div>
)}
</div>
const PreviewPresentation = ({
editable,
faved,
favoriteCount,
isFullScreen,
isLoggedIn,
isShared,
loved,
loveCount,
originalInfo,
parentInfo,
projectId,
projectInfo,
remixes,
report,
studios,
userOwnsProject,
onFavoriteClicked,
onLoveClicked,
onReportClicked,
onReportClose,
onReportSubmit,
onSeeInside,
onUpdate
}) => {
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : '';
return (
<div className="preview">
<ShareBanner shared={isShared} />
);
}
}
{ projectInfo && projectInfo.author && projectInfo.author.id && (
<div className="inner">
<Formsy>
<FlexRow className="preview-row">
<FlexRow className="project-header">
<a href={`/users/${projectInfo.author.username}`}>
<Avatar
alt={projectInfo.author.username}
src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo. author.id}_48x48.png`}
/>
</a>
<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={{
maxLength: 100
}}
value={projectInfo.title}
/> :
<React.Fragment>
<div className="project-title">{projectInfo.title}</div>
{'by '}
<a href={`/users/${projectInfo.author.username}`}>
{projectInfo.author.username}
</a>
</React.Fragment>
}
</div>
</FlexRow>
<div className="project-buttons">
{/* TODO: Hide Remix button for now until implemented */}
{(!userOwnsProject && false) &&
<Button className="button remix-button">
Remix
</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">
<RemixCredit projectInfo={parentInfo} />
<RemixCredit projectInfo={originalInfo} />
{/* eslint-disable max-len */}
<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"
placeholder="Tell people how to use your project (such as which keys to press)."
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',
'last',
{remixes: parentInfo && parentInfo.author}
)}
handleUpdate={onUpdate}
name="description"
placeholder="How did you make this project? Did you use ideas scripts or artwork from other people? Thank them here."
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>
{/* eslint-enable max-len */}
</FlexRow>
</FlexRow>
<FlexRow className="preview-row">
<FlexRow className="stats">
<div
className={classNames('project-loves', {loved: loved})}
key="loves"
onClick={onLoveClicked}
>
{approx(loveCount, {decimal: false})}
</div>
<div
className={classNames('project-favorites', {favorited: faved})}
key="favorites"
onClick={onFavoriteClicked}
>
{approx(favoriteCount, {decimal: false})}
</div>
<div
className="project-remixes"
key="remixes"
>
{approx(projectInfo.stats.remixes, {decimal: false})}
</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>
{(isLoggedIn && !userOwnsProject) &&
<React.Fragment>
<Button
className="action-button report-button"
key="report-button"
onClick={onReportClicked}
>
Report
</Button>,
<ReportModal
key="report-modal"
report={report}
type="project"
onReport={onReportSubmit}
onRequestClose={onReportClose}
/>
</React.Fragment>
}
</FlexRow>
</FlexRow>
</FlexRow>
<FlexRow className="preview-row">
<div className="comments-container">
<div className="project-title" />
</div>
<FlexRow className="column">
<RemixList remixes={remixes} />
<StudioList studios={studios} />
</FlexRow>
</FlexRow>
</Formsy>
</div>
)}
</div>
);
};
PreviewPresentation.propTypes = {
editable: PropTypes.bool,
faved: PropTypes.bool,
favoriteCount: PropTypes.number,
isFullScreen: PropTypes.bool,
isLoggedIn: PropTypes.bool,
isShared: PropTypes.bool,
loveCount: PropTypes.number,
loved: PropTypes.bool,
onFavoriteClicked: PropTypes.func,
onLoveClicked: PropTypes.func,
onReportClicked: PropTypes.func.isRequired,
onReportClose: PropTypes.func.isRequired,
onReportSubmit: PropTypes.func.isRequired,
onSeeInside: PropTypes.func,
onUpdate: PropTypes.func,
originalInfo: projectShape,
@ -412,18 +299,14 @@ PreviewPresentation.propTypes = {
projectId: PropTypes.string,
projectInfo: projectShape,
remixes: PropTypes.arrayOf(PropTypes.object),
sessionStatus: PropTypes.string.isRequired,
report: PropTypes.shape({
category: PropTypes.string,
notes: PropTypes.string,
open: PropTypes.bool,
waiting: PropTypes.bool
}),
studios: PropTypes.arrayOf(PropTypes.object),
user: PropTypes.shape({
id: PropTypes.number,
banned: PropTypes.bool,
username: PropTypes.string,
token: PropTypes.string,
thumbnailUrl: PropTypes.string,
dateJoined: PropTypes.string,
email: PropTypes.string,
classroomId: PropTypes.string
}).isRequired
userOwnsProject: PropTypes.bool
};
module.exports = injectIntl(PreviewPresentation);

View file

@ -24,12 +24,33 @@ class Preview extends React.Component {
'handleLoveToggle',
'handlePermissions',
'handlePopState',
'handleReportClick',
'handleReportClose',
'handleReportSubmit',
'handleSeeInside',
'handleUpdate',
'initCounts',
'pushHistory'
'isShared',
'pushHistory',
'userOwnsProject'
]);
this.state = this.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'
this.state = {
editable: false,
favoriteCount: 0,
loveCount: 0,
projectId: parts[1] === 'editor' ? 0 : parts[1],
report: {
category: '',
notes: '',
open: false,
waiting: false
}
};
this.addEventListeners();
}
componentDidUpdate (prevProps) {
@ -70,25 +91,39 @@ 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);
}
removeEventListeners () {
window.removeEventListener('popstate', this.handlePopState);
}
handleReportClick () {
this.setState({report: {...this.state.report, open: true}});
}
handleReportClose () {
this.setState({report: {...this.state.report, open: false}});
}
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}
});
}
handlePopState () {
const path = window.location.pathname.toLowerCase();
const playerMode = path.indexOf('editor') === -1;
@ -179,6 +214,28 @@ class Preview extends React.Component {
loveCount: loves
});
}
isShared () {
return (
// if we don't have projectInfo assume shared until we know otherwise
Object.keys(this.props.projectInfo).length === 0 || (
this.props.projectInfo.history &&
this.props.projectInfo.history.shared.length > 0
)
);
}
isLoggedIn () {
return (
this.props.sessionStatus === sessionActions.Status.FETCHED &&
Object.keys(this.props.user).length > 0
);
}
userOwnsProject () {
return (
this.isLoggedIn() &&
Object.keys(this.props.projectInfo).length > 0 &&
this.props.user.id === this.props.projectInfo.author.id
);
}
render () {
return (
this.props.playerMode ?
@ -189,6 +246,8 @@ class Preview extends React.Component {
faved={this.props.faved}
favoriteCount={this.state.favoriteCount}
isFullScreen={this.state.isFullScreen}
isLoggedIn={this.isLoggedIn()}
isShared={this.isShared()}
loveCount={this.state.loveCount}
loved={this.props.loved}
originalInfo={this.props.original}
@ -196,11 +255,15 @@ class Preview extends React.Component {
projectId={this.state.projectId}
projectInfo={this.props.projectInfo}
remixes={this.props.remixes}
sessionStatus={this.props.sessionStatus}
report={this.state.report}
studios={this.props.studios}
user={this.props.user}
userOwnsProject={this.userOwnsProject()}
onFavoriteClicked={this.handleFavoriteToggle}
onLoveClicked={this.handleLoveToggle}
onReportClicked={this.handleReportClick}
onReportClose={this.handleReportClose}
onReportSubmit={this.handleReportSubmit}
onSeeInside={this.handleSeeInside}
onUpdate={this.handleUpdate}
/>

View file

@ -1,7 +1,7 @@
@import "../../colors";
@import "../../frameless";
/* stage size contants
/* stage size constants
* this is a hack right now - stage includes padding of .5rem (8px) for alignment in gui
* in www the player is placed with margin -.5rem to align the edge.
* the height is calculated from the actual height on the page (404)
@ -10,13 +10,6 @@ $gui-width: 496px;
$stage-width: 480px;
$stage-height: 404px;
// remix credit height: 52px
// project text label line-height + margin-bottom .5rem: 19px + 8px = 27px
// Formsy wrapper adds 3px to the input height for
$description-input: 166px; // $stage-height / 2 - $project-label - $wrapper - margin
$description-input-small: 120px; // normal $description-input - $remix-credit
/* override view padding for share banner */
#view {
padding: 0 0 20px 0;
@ -138,7 +131,6 @@ $description-input-small: 120px; // normal $description-input - $remix-credit
width: 60%;
}
.share-button,
.remix-button,
.see-inside-button {
margin-top: 0;
@ -158,18 +150,6 @@ $description-input-small: 120px; // normal $description-input - $remix-credit
}
}
.shareText {
align-self: center;
}
.share-button {
background-color: $ui-orange;
&:before {
background-image: url("/svgs/project/share-white.svg");
}
}
.remix-button {
background-color: $ui-green;
@ -189,6 +169,7 @@ $description-input-small: 120px; // normal $description-input - $remix-credit
margin-top: 1rem;
justify-content: space-between;
align-items: flex-start;
flex-wrap: nowrap;
}
.guiPlayer {
@ -237,10 +218,11 @@ $description-input-small: 120px; // normal $description-input - $remix-credit
}
.description-block {
display: flex;
width: 100%;
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
flex: 1;
}
.project-textlabel {
@ -256,6 +238,7 @@ $description-input-small: 120px; // normal $description-input - $remix-credit
background-color: $ui-blue-10percent;
padding: .5rem;
width: calc(100% - (1rem + 2px));
overflow: auto;
white-space: pre-line;
font-size: 1rem;
// flex-grow
@ -267,6 +250,7 @@ $description-input-small: 120px; // normal $description-input - $remix-credit
}
.project-description-edit {
display: flex;
margin-bottom: .75rem;
border: 1px solid $ui-blue-10percent;
border-radius: 8px;
@ -293,15 +277,12 @@ $description-input-small: 120px; // normal $description-input - $remix-credit
}
}
.inplace-textarea {
height: $description-input;
& > .grow {
display: flex;
flex: 1;
}
}
.project-description-edit.remixes .inplace-textarea {
height: $description-input-small;
}
.copyleft {
display: inline-block;
transform: scale(-1, 1);

View file

@ -0,0 +1,34 @@
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Avatar = require('../../components/avatar/avatar.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
const RemixCredit = props => {
const projectInfo = props.projectInfo;
if (Object.keys(projectInfo).length === 0) return null;
return (
<FlexRow className="remix-credit">
<Avatar
className="remix"
src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo.author.id}_48x48.png`}
/>
<div className="credit-text">
Thanks to <a
href={`/users/${projectInfo.author.username}`}
>
{projectInfo.author.username}
</a> for the original project <a
href={`/preview/${projectInfo.id}`}
>
{projectInfo.title}
</a>.
</div>
</FlexRow>
);
};
RemixCredit.propTypes = {
projectInfo: projectShape
};
module.exports = RemixCredit;

View file

@ -0,0 +1,37 @@
const React = require('react');
const PropTypes = require('prop-types');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const ThumbnailColumn = require('../../components/thumbnailcolumn/thumbnailcolumn.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
const RemixList = props => {
const remixes = props.remixes;
if (remixes.length === 0) return null;
return (
<FlexRow className="remix-list">
<div className="project-title">
Remixes
</div>
{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>
);
};
RemixList.propTypes = {
remixes: PropTypes.arrayOf(projectShape)
};
module.exports = RemixList;

View file

@ -0,0 +1,30 @@
const PropTypes = require('prop-types');
const React = require('react');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx');
require('./share-banner.scss');
const ShareBanner = props => {
if (props.shared) return null;
return (
<div className="shareBanner">
<div className="inner">
<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>
</div>
</div>
);
};
ShareBanner.propTypes = {
shared: PropTypes.bool.isRequired
};
module.exports = ShareBanner;

View file

@ -0,0 +1,30 @@
@import "../../colors";
$navigation-height: 50px;
.shareBanner {
background-color: $ui-orange-25percent;
width: 100%;
overflow: hidden;
color: $ui-orange;
}
.share-button {
margin-top: 0;
background-color: $ui-orange;
font-size: .875rem;
font-weight: normal;
&:before {
display: inline-block;
margin-right: .5rem;
background-image: url("/svgs/project/share-white.svg");
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
width: 1.25rem;
height: 1.25rem;
vertical-align: middle;
content: "";
}
}

View file

@ -0,0 +1,37 @@
const React = require('react');
const PropTypes = require('prop-types');
const FlexRow = require('../../components/flex-row/flex-row.jsx');
const ThumbnailColumn = require('../../components/thumbnailcolumn/thumbnailcolumn.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
const StudioList = props => {
const studios = props.studios;
if (studios.length === 0) return null;
return (
<FlexRow className="remix-list">
<div className="project-title">
Studios
</div>
{studios.length === 0 ? (
// TODO: style remix invitation
<span>Invite user to add to studio</span>
) : (
<ThumbnailColumn
cards
showAvatar
itemType="studio"
items={studios.slice(0, 5)}
showFavorites={false}
showLoves={false}
showViews={false}
/>
)}
</FlexRow>
);
};
StudioList.propTypes = {
studios: PropTypes.arrayOf(projectShape)
};
module.exports = StudioList;