Merge branch 'develop' of https://github.com/LLK/scratch-www into develop

This commit is contained in:
kathy wu 2018-10-09 18:15:22 -04:00
commit a224e82c41
20 changed files with 1022 additions and 537 deletions

View file

@ -38,6 +38,29 @@ $desktop: 942px;
$tablet: 640px;
$mobile: 480px;
/* Media Queries */
/* Width */
/*
* ... small | medium | intermediate | big ...
* ... medium-and-smaller |
* ... intermediate-and-smaller |
*/
$small: "only screen and (max-width : #{$mobile}-1)";
$medium: "only screen and (min-width : #{$mobile}) and (max-width : #{$tablet}-1)";
$intermediate: "only screen and (min-width : #{$tablet}) and (max-width : #{$desktop}-1)";
$big: "only screen and (min-width : #{$desktop})";
$medium-and-smaller: "only screen and (max-width : #{$tablet}-1)";
$intermediate-and-smaller: "only screen and (max-width : #{$desktop}-1)";
/* Height */
$small-height: "only screen and (max-height : #{$mobile} - 1)";
$medium-height: "only screen and (min-height : #{$mobile}) and (max-height : #{$tablet} - 1)";
//
// Column-widths in a function, in ems
//

View file

@ -36,7 +36,7 @@
}
.inplace-textarea {
transition: all 1s ease;
transition: all .2s ease;
border: 2px dashed $ui-blue-25percent;
border-radius: 8px;
background-color: $ui-light-gray;
@ -49,7 +49,7 @@
resize: none;
&:focus {
transition: all 1s ease;
transition: all .2s ease;
outline: none;
border: 2px solid $ui-blue;
box-shadow: 0 0 0 4px $ui-blue-25percent;

View file

@ -1,52 +1,41 @@
@import "../../../colors";
@import "../../../frameless";
.mod-addToStudio * {
box-sizing: border-box;
}
.mod-addToStudio {
margin: 100px auto;
outline: none;
padding: 0;
width: 36.25rem; /* 580px; */
height: 388px; /* 24.25rem; */
overflow: hidden;
user-select: none;
@media #{$small}, #{$small-height} {
overflow: hidden;
}
}
.addToStudio-modal-header {
box-shadow: inset 0 -1px 0 0 $ui-blue-dark;
background-color: $ui-blue;
padding-top: .75rem;
width: 100%;
height: 3rem;
box-sizing: border-box;
}
.addToStudio-content-label {
text-align: center;
color: $type-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: bold;
}
.addToStudio-modal-content {
margin: 0 auto;
box-shadow: none;
width: 100%;
font-size: .875rem;
}
.studio-list-outer-scrollbox {
position: relative;
background-color: $ui-blue-10percent;
min-height: 15rem;
max-height: calc(100% - 8rem);
flex: 1;
@media #{$small-height} {
min-height: 0;
}
}
.studio-list-inner-scrollbox {
margin-right: .5rem;
padding-right: .5rem;
height: 16.9375rem;
height: 100%;
overflow: scroll;
overflow-x: hidden;
@ -93,35 +82,32 @@
display: flex;
position: relative;
transition: all .5s;
margin: .21875rem .21875rem;
margin: .21875rem;
border-radius: .5rem;
background-color: $ui-white;
cursor: pointer;
padding: 0;
width: 16.1875rem; /* 259px */
width: 48%;
height: 2.5rem;
box-sizing: border-box;
justify-content: space-between;
align-items: center;
@media #{$small} {
min-width: 98%;
flex-shrink: 1;
}
}
.studio-selector-button-text {
position: absolute;
/* per spec, should be:
margin: .375rem 2.18375rem .375rem .6875rem
but in practice, our css seems to vertically align text to top, where
invision spec aligned to middle.
*/
margin: .575rem 2.18375rem .175rem .6875rem;
width: 13.3125rem;
height: 1.25rem; /* diff from spec, in case we ever do valign to middle; changed to match line-height because else with overflow hidden it cuts off some letters */
margin: auto 2.18375rem auto .6875rem;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.25rem;
white-space: nowrap;
font-family: "Helvetica Neue";
font-size: .875rem;
font-weight: regular;
flex-shrink: 1;
}
.studio-selector-button-selected {
@ -144,7 +130,7 @@
.studio-status-icon {
position: absolute;
margin: .5rem .625rem .5rem 14.0625rem;
right: .625rem;
border-radius: .75rem;
padding: .0625rem .075rem;
width: 1.5rem;

View file

@ -37,70 +37,69 @@ const AddToStudioModalPresentation = ({
return (
<Modal
useStandardSizes
className="mod-addToStudio"
contentLabel={contentLabel}
isOpen={isOpen}
onRequestClose={onRequestClose}
>
<div>
<div className="addToStudio-modal-header">
<div className="addToStudio-content-label">
{contentLabel}
<div className="addToStudio-modal-header modal-header">
<div className="addToStudio-content-label content-label">
{contentLabel}
</div>
</div>
<div className="addToStudio-modal-content modal-content">
<div className="studio-list-outer-scrollbox">
<div className="studio-list-inner-scrollbox">
<div className="studio-list-container">
{studioButtons}
</div>
<div className="studio-list-bottom-gradient" />
</div>
</div>
<div className="addToStudio-modal-content">
<div className="studio-list-outer-scrollbox">
<div className="studio-list-inner-scrollbox">
<div className="studio-list-container">
{studioButtons}
<Form
className="add-to-studio"
onSubmit={onSubmit}
>
<FlexRow className="action-buttons">
<Button
className="action-button close-button white"
key="closeButton"
name="closeButton"
type="button"
onClick={onRequestClose}
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
<div className="studio-list-bottom-gradient" />
</div>
</div>
<Form
className="add-to-studio"
onSubmit={onSubmit}
>
<FlexRow className="action-buttons">
</Button>
{waitingToClose ? [
<Button
className="action-button close-button white"
key="closeButton"
name="closeButton"
type="button"
onClick={onRequestClose}
className="action-button submit-button submit-button-waiting"
disabled="disabled"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
<Spinner />
<FormattedMessage id="addToStudio.finishing" />
</div>
</Button>
{waitingToClose ? [
<Button
className="action-button submit-button submit-button-waiting"
disabled="disabled"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<Spinner />
<FormattedMessage id="addToStudio.finishing" />
</div>
</Button>
] : [
<Button
className="action-button submit-button"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<FormattedMessage id="general.okay" />
</div>
</Button>
]}
</FlexRow>
</Form>
</div>
] : [
<Button
className="action-button submit-button"
key="submitButton"
type="submit"
>
<div className="action-button-text">
<FormattedMessage id="general.okay" />
</div>
</Button>
]}
</FlexRow>
</Form>
</div>
</Modal>
);

View file

@ -26,8 +26,11 @@ class Modal extends React.Component {
return (
<ReactModal
appElement={document.getElementById('app')}
bodyOpenClassName={this.props.useStandardSizes ? classNames('overflow-hidden') : null}
className={{
base: classNames('modal-content', this.props.className),
base: classNames('modal-content', this.props.className, {
'modal-sizes': this.props.useStandardSizes
}),
afterOpen: classNames('modal-content', this.props.className),
beforeClose: classNames('modal-content', this.props.className)
}}
@ -60,7 +63,8 @@ class Modal extends React.Component {
Modal.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
overlayClassName: PropTypes.string
overlayClassName: PropTypes.string,
useStandardSizes: PropTypes.bool
};
module.exports = Modal;

View file

@ -1,6 +1,12 @@
@import "../../../colors";
@import "../../../frameless";
.overflow-hidden {
/* to avoid double scroll bars this
gets added to body while modal is open */
overflow: hidden;
}
.modal-content {
position: relative;
margin: 3.75rem auto;
@ -10,9 +16,27 @@
padding: 0;
width: 48.75rem;
.modal-content { /* content inside of content */
display: flex;
border-radius: 0;
flex-direction: column;
}
&:focus {
outline: none;
}
@media #{$intermediate-and-smaller} {
margin-top: 0;
width: 100%;
overflow: auto;
}
@media #{$small}, #{$small-height} {
border-radius: 0;
box-shadow: none;
height: 100%;
}
}
.modal-overlay {
@ -43,23 +67,6 @@ $modal-close-size: 1rem;
padding-top: $modal-close-size / 2;
}
@media only screen and (max-width: $desktop - 1) {
.modal-content {
top: 0;
left: 0;
margin-top: 0;
border-radius: 0;
box-shadow: none;
width: 100%;
height: 100%;
overflow: scroll;
}
.modal-content-close {
position: fixed;
}
}
/* Close button, Submit button, etc. */
.action-buttons {
display: flex;
@ -68,6 +75,11 @@ $modal-close-size: 1rem;
justify-content: flex-end !important;
align-items: flex-start;
flex-wrap: nowrap;
@media #{$intermediate-and-smaller} {
justify-content: center !important; //overwriting flex row properties
flex-direction: row !important;
}
}
/* setting overall modal to contain overflow looks good, but isn't
@ -108,3 +120,46 @@ row to appear to contain overflow. */
overflow: visible;
color: $type-white;
}
.modal-sizes * {
box-sizing: border-box;
}
.modal-sizes {
margin: 100px auto;
outline: none;
padding: 0;
max-width: 36.25rem; /* 580px; */
user-select: none;
@media #{$medium}, #{$medium-height} {
margin: 40px auto;
}
@media #{$small}, #{$small-height} {
margin: 0 auto;
width: auto;
}
.modal-header {
padding-top: .75rem;
width: 100%;
height: 3rem;
@media #{$small}, #{$small-height} {
border-radius: 0;
}
}
.content-label {
text-align: center;
color: $type-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: bold;
}
.modal-content {
font-size: .875rem;
}
}

View file

@ -111,6 +111,7 @@ class ReportModal extends React.Component {
const contentLabel = intl.formatMessage({id: `report.${type}`});
return (
<Modal
useStandardSizes
className="mod-report"
contentLabel={contentLabel}
isOpen={isOpen}
@ -118,8 +119,8 @@ class ReportModal extends React.Component {
{...modalProps}
>
<div>
<div className="report-modal-header">
<div className="report-content-label">
<div className="report-modal-header modal-header">
<div className="report-content-label content-label">
{contentLabel}
</div>
</div>
@ -130,7 +131,7 @@ class ReportModal extends React.Component {
onValid={this.handleValid}
onValidSubmit={onReport}
>
<div className="report-modal-content">
<div className="report-modal-content modal-content">
{isConfirmed ? (
<div className="received">
<div className="received-header">

View file

@ -1,42 +1,15 @@
@import "../../../colors";
@import "../../../frameless";
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
.mod-report * {
box-sizing: border-box;
}
.mod-report {
margin: 100px auto;
outline: none;
padding: 0;
width: 36.25rem; /* 580px; */
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;
width: 100%;
height: 3rem;
box-sizing: border-box;
}
.report-content-label {
text-align: center;
color: $type-white;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: bold;
}
.report-modal-content {
margin: 1rem auto;
width: 80%;
font-size: .875rem;
.instructions {
line-height: 1.5rem;
@ -73,7 +46,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
overflow: visible;
color: $type-white;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
position: relative;
margin-top: calc($arrow-border-width / 2);
max-width: 100%;
@ -98,7 +71,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
content: "";
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
display: none;
}
}

View file

@ -1,6 +1,6 @@
import ScratchStorage from 'scratch-storage';
const PROJECT_SERVER = 'https://projects.scratch.mit.edu';
const PROJECT_HOST = process.env.PROJECT_HOST || 'https://projects.scratch.mit.edu';
/**
* Wrapper for ScratchStorage which adds default web sources.
@ -14,8 +14,8 @@ class Storage extends ScratchStorage {
projectAsset => {
const [projectId, revision] = projectAsset.assetId.split('.');
return revision ?
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/${revision}` :
`${PROJECT_SERVER}/internalapi/project/${projectId}/get/`;
`${PROJECT_HOST}/internalapi/project/${projectId}/get/${revision}` :
`${PROJECT_HOST}/internalapi/project/${projectId}/get/`;
}
);
}

View file

@ -86,6 +86,43 @@ module.exports.previewReducer = (state, action) => {
return Object.assign({}, state, {
comments: [...state.comments, ...action.items] // TODO: consider a different way of doing this?
});
case 'SET_COMMENT_DELETED':
if (action.topLevelCommentId) {
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].map(comment => {
if (comment.id === action.commentId) {
return Object.assign({}, comment, {deleted: true});
}
return comment;
})
})
});
}
return Object.assign({}, state, {
comments: state.comments.map(comment => {
if (comment.id === action.commentId) {
return Object.assign({}, comment, {deleted: true});
}
return comment;
})
});
case 'ADD_NEW_COMMENT':
if (action.topLevelCommentId) {
return Object.assign({}, state, {
replies: Object.assign({}, state.replies, {
// Replies to comments go at the end of the thread
[action.topLevelCommentId]: state.replies[action.topLevelCommentId].concat(action.comment)
})
});
}
// Reply to the top level project, put the reply at the beginning
return Object.assign({}, state, {
comments: [action.comment, ...state.comments],
replies: Object.assign({}, state.replies, {[action.comment.id]: []})
});
case 'SET_REPLIES':
return Object.assign({}, state, {
replies: merge({}, state.replies, action.replies)
@ -191,6 +228,18 @@ module.exports.setStudioFetchStatus = (studioId, status) => ({
status: status
});
module.exports.setCommentDeleted = (commentId, topLevelCommentId) => ({
type: 'SET_COMMENT_DELETED',
commentId: commentId,
topLevelCommentId: topLevelCommentId
});
module.exports.addNewComment = (comment, topLevelCommentId) => ({
type: 'ADD_NEW_COMMENT',
comment: comment,
topLevelCommentId: topLevelCommentId
});
module.exports.getProjectInfo = (id, token) => (dispatch => {
const opts = {
uri: `/projects/${id}`
@ -562,6 +611,26 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
});
});
module.exports.deleteComment = (projectId, commentId, token) => (dispatch => {
/* TODO fetching/fetched/error states updates for comment deleting */
api({
uri: `/proxy/comments/project/${projectId}`,
authentication: token,
withCredentials: true,
method: 'DELETE',
useCsrf: true,
json: {
id: commentId
}
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(module.exports.setCommentDeleted(commentId));
});
});
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

View file

@ -1,59 +1,122 @@
const React = require('react');
const PropTypes = require('prop-types');
const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
const Avatar = require('../../../components/avatar/avatar.jsx');
const FormattedRelative = require('react-intl').FormattedRelative;
const ComposeComment = require('./compose-comment.jsx');
require('./comment.scss');
const Comment = ({
author,
content,
datetimeCreated,
id
}) => (
<div
className="flex-row comment"
id={`comments-${id}`}
>
<a href={`/users/${author.username}`}>
<Avatar src={author.image} />
</a>
<FlexRow className="comment-body column">
<FlexRow className="comment-top-row">
<a
className="username"
href={`/users/${author.username}`}
>{author.username}</a>
<div className="action-list">
{/* TODO: Hook these up to API calls/logic */}
<span className="comment-delete">Delete</span>
<span className="comment-report">Report</span>
</div>
</FlexRow>
<div className="comment-bubble">
{/* TODO: at the moment, comment content does not properly display
* emojis/easter eggs
* @user links in replies
* links to scratch.mit.edu pages
*/}
<span className="comment-content">{content}</span>
<FlexRow className="comment-bottom-row">
<span className="comment-time">
<FormattedRelative value={new Date(datetimeCreated)} />
</span>
<a
className="comment-reply"
href={`#comments-${id}`}
class Comment extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleDelete',
'handlePostReply',
'handleToggleReplying'
]);
this.state = {
replying: false
};
}
handlePostReply (comment) {
this.setState({replying: false});
this.props.onAddComment(comment);
}
handleToggleReplying () {
this.setState({replying: !this.state.replying});
}
handleDelete () {
this.props.onDelete(this.props.id);
}
render () {
const {
author,
deletable,
deleted,
canReply,
content,
datetimeCreated,
id,
projectId
} = this.props;
return (
<div
className="flex-row comment"
id={`comments-${id}`}
>
<a href={`/users/${author.username}`}>
<Avatar src={author.image} />
</a>
<FlexRow className="comment-body column">
<FlexRow className="comment-top-row">
<a
className="username"
href={`/users/${author.username}`}
>{author.username}</a>
<div className="action-list">
{deletable ? (
<span
className="comment-delete"
onClick={this.handleDelete}
>
Delete {/* TODO internationalize */}
</span>
) : null}
<span className="comment-report">
Report {/* TODO internationalize */}
</span>
</div>
</FlexRow>
<div
className={classNames({
'comment-bubble': true,
'comment-bubble-deleted': deleted
})}
>
reply
</a>
{/* TODO: at the moment, comment content does not properly display
* emojis/easter eggs
* @user links in replies
* links to scratch.mit.edu pages
*/}
<span className="comment-content">{content}</span>
<FlexRow className="comment-bottom-row">
<span className="comment-time">
<FormattedRelative value={new Date(datetimeCreated)} />
</span>
{canReply ? (
<span
className="comment-reply"
onClick={this.handleToggleReplying}
>
reply
</span>
) : null}
</FlexRow>
</div>
{this.state.replying ? (
<FlexRow className="comment-reply-row">
<ComposeComment
parentId={id}
projectId={projectId}
onAddComment={this.handlePostReply}
onCancel={this.handleToggleReplying}
/>
</FlexRow>
) : null}
</FlexRow>
</div>
</FlexRow>
</div>
);
);
}
}
Comment.propTypes = {
author: PropTypes.shape({
@ -61,9 +124,15 @@ Comment.propTypes = {
image: PropTypes.string,
username: PropTypes.string
}),
canReply: PropTypes.bool,
content: PropTypes.string,
datetimeCreated: PropTypes.string,
id: PropTypes.number
deletable: PropTypes.bool,
deleted: PropTypes.bool,
id: PropTypes.number,
onAddComment: PropTypes.func,
onDelete: PropTypes.func,
projectId: PropTypes.number
};
module.exports = Comment;

View file

@ -1,8 +1,26 @@
@import "../../../colors";
.compose-comment {
margin-left: .5rem;
width: 100%;
.compose-error-row {
width: 100%;
justify-content: flex-start;
.compose-error-tip {
margin-bottom: .5rem;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: .25rem;
width: 100%;
text-align: left;
color: $type-white;
font-size: .85rem;
}
}
.textarea-row {
width: 100%;
@ -131,6 +149,19 @@
height: 9px;
content: "";
}
&.comment-bubble-deleted {
$deleted-outline: #ff6680;
$deleted-background: rgb(236, 206, 223);
border-color: $deleted-outline;
background-color: $deleted-background;
&:before {
border-color: $deleted-outline transparent $deleted-outline $deleted-outline;
background: $deleted-background;
}
}
}
.comment-content {
@ -148,6 +179,9 @@
.comment-reply {
display: inline-flex;
cursor: pointer;
color: $ui-blue;
font-weight: bold;
&:after {
margin-left: .25rem;
@ -184,6 +218,16 @@
}
}
.comments-root-reply {
margin-bottom: 1.5rem;
}
.comment-reply-row {
margin-top: 1.5rem;
margin-left: .5rem;
width: 100%;
}
.expand-thread {
margin-bottom: 24px;
width: 100%;

View file

@ -1,26 +1,203 @@
const React = require('react');
const PropTypes = require('prop-types');
const bindAll = require('lodash.bindall');
const classNames = require('classnames');
const keyMirror = require('keymirror');
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
const Avatar = require('../../../components/avatar/avatar.jsx');
const InplaceInput = require('../../../components/forms/inplace-input.jsx');
const Button = require('../../../components/forms/button.jsx');
const connect = require('react-redux').connect;
const api = require('../../../lib/api');
require('./comment.scss');
const onUpdate = update => update;
const ComposeComment = () => (
<FlexRow className="compose-comment column">
<InplaceInput
handleUpdate={onUpdate}
name="compose-comment"
type="textarea"
/>
<FlexRow className="compose-bottom-row">
<Button className="compose-post">Post</Button>
<Button className="compose-cancel">Cancel</Button>
<span className="compose-limit">500 characters left</span>
</FlexRow>
</FlexRow>
);
const MAX_COMMENT_LENGTH = 500;
module.exports = ComposeComment;
const ComposeStatus = keyMirror({
EDITING: null,
SUBMITTING: null,
REJECTED: null
});
/* TODO translations */
const CommentErrorMessages = {
isEmpty: "You can't post an empty comment",
isFlood: "Woah, seems like you're commenting really quickly. Please wait longer between posts.",
isBad: 'Hmm...the bad word detector thinks there is a problem with your comment. ' +
'Please change it and remember to be respectful.',
hasChatSite: 'Uh oh! This comment contains a link to a website with unmoderated chat.' +
'For safety reasons, please do not link to these sites!',
isSpam: "Hmm, seems like you've posted the same comment a bunch of times. Please don't spam.",
isMuted: "Hmm, the filterbot is pretty sure your recent comments weren't ok for Scratch, " +
'so your account has been muted for the rest of the day. :/',
isUnconstructive: 'Hmm, the filterbot thinks your comment may be mean or disrespectful. ' +
'Remember, most projects on Scratch are made by people who are just learning how to program.',
isDisallowed: 'Hmm, it looks like comments have been turned off for this page. :/',
// TODO implement the special modal for ip mute bans that includes links to appeals
// this is just a stub of the actual message
isIPMuted: 'Sorry, the Scratch Team had to prevent your network from sharing comments or ' +
'projects because it was used to break our community guidelines too many times.' +
'You can still share comments and projects from another network.',
isTooLong: "That's too long! Please find a way to shorten your text.",
error: 'Oops! Something went wrong'
};
class ComposeComment extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handlePost',
'handleCancel',
'handleInput'
]);
this.state = {
message: '',
status: ComposeStatus.EDITING,
error: null
};
}
handleInput (event) {
this.setState({
message: event.target.value,
status: ComposeStatus.EDITING,
error: null
});
}
handlePost () {
this.setState({status: ComposeStatus.SUBMITTING});
api({
uri: `/proxy/comments/project/${this.props.projectId}`,
authentication: this.props.user.token,
withCredentials: true,
method: 'POST',
useCsrf: true,
json: {
content: this.state.message,
parent_id: this.props.parentId || '',
comentee_id: this.props.comenteeId || ''
}
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
body = {rejected: 'error'};
}
if (body.rejected && this.state.status === ComposeStatus.SUBMITTING) {
// Note: does not reset the message state
this.setState({
status: ComposeStatus.REJECTED,
// If there is a special error message for the rejected reason,
// use it. Otherwise, use the generic 'error' ("Oops!...")
error: CommentErrorMessages[body.rejected] ?
body.rejected : 'error'
});
return;
}
// Clear the text field and reset status on successful submission
this.setState({
message: '',
status: ComposeStatus.EDITING,
error: null
});
// Add the username, which isn't included right now from scratch-api
if (body.author) body.author.username = this.props.user.username;
this.props.onAddComment(body);
});
}
handleCancel () {
this.setState({
message: '',
status: ComposeStatus.EDITING,
error: null
});
if (this.props.onCancel) this.props.onCancel();
}
render () {
return (
<div
className="flex-row comment"
>
<a href={`/users/${this.props.user.username}`}>
<Avatar src={this.props.user.thumbnailUrl} />
</a>
<FlexRow className="compose-comment column">
{this.state.error ? (
<FlexRow className="compose-error-row">
<div className="compose-error-tip">
{CommentErrorMessages[this.state.error] || 'Unknown error'}
</div>
</FlexRow>
) : null}
<InplaceInput
className={classNames('compose-input',
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ? 'compose-valid' : 'compose-invalid')}
handleUpdate={onUpdate}
name="compose-comment"
type="textarea"
value={this.state.message}
onInput={this.handleInput}
/>
<FlexRow className="compose-bottom-row">
<Button
className="compose-post"
disabled={this.state.status === ComposeStatus.SUBMITTING}
onClick={this.handlePost}
>
{this.state.status === ComposeStatus.SUBMITTING ? (
'Posting...' /* TODO internationalize */
) : (
'Post' /* TODO internationalize */
)}
</Button>
<Button
className="compose-cancel"
onClick={this.handleCancel}
>
Cancel {/* TODO internationalize */}
</Button>
<span
className={classNames('compose-limit',
MAX_COMMENT_LENGTH - this.state.message.length >= 0 ?
'compose-valid' : 'compose-invalid')}
>
{/* TODO internationalize */}
{MAX_COMMENT_LENGTH - this.state.message.length} characters left
</span>
</FlexRow>
</FlexRow>
</div>
);
}
}
ComposeComment.propTypes = {
comenteeId: PropTypes.number,
onAddComment: PropTypes.func,
onCancel: PropTypes.func,
parentId: PropTypes.number,
projectId: PropTypes.number,
user: PropTypes.shape({
id: PropTypes.number,
username: PropTypes.string,
token: PropTypes.string,
thumbnailUrl: PropTypes.string
})
};
const mapStateToProps = state => ({
user: state.session.session.user
});
const ConnectedComposeComment = connect(
mapStateToProps
)(ComposeComment);
module.exports = ConnectedComposeComment;

View file

@ -12,7 +12,9 @@ class TopLevelComment extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleExpandThread'
'handleExpandThread',
'handleAddComment',
'handleDeleteReply'
]);
this.state = {
expanded: false
@ -25,18 +27,37 @@ class TopLevelComment extends React.Component {
});
}
handleDeleteReply (commentId) {
// Only apply topLevelCommentId for deleting replies
// The top level comment itself just gets passed onDelete directly
this.props.onDelete(commentId, this.props.id);
}
handleAddComment (comment) {
this.props.onAddComment(comment, this.props.id);
}
render () {
const {
author,
canReply,
content,
datetimeCreated,
deletable,
deleted,
id,
replies
onDelete,
replies,
projectId
} = this.props;
return (
<FlexRow className="comment-container">
<Comment {...{author, content, datetimeCreated, id}} />
<Comment
projectId={projectId}
onAddComment={this.handleAddComment}
{...{author, content, datetimeCreated, deletable, deleted, canReply, id, onDelete}}
/>
{replies.length > 0 &&
<FlexRow
className={classNames(
@ -49,10 +70,16 @@ class TopLevelComment extends React.Component {
{(this.state.expanded ? replies : replies.slice(0, 3)).map(reply => (
<Comment
author={reply.author}
canReply={canReply}
content={reply.content}
datetimeCreated={reply.datetime_created}
deletable={deletable}
deleted={reply.deleted}
id={reply.id}
key={reply.id}
projectId={projectId}
onAddComment={this.handleAddComment}
onDelete={this.handleDeleteReply}
/>
))}
</FlexRow>
@ -74,9 +101,14 @@ TopLevelComment.propTypes = {
image: PropTypes.string,
username: PropTypes.string
}),
canReply: PropTypes.bool,
content: PropTypes.string,
datetimeCreated: PropTypes.string,
deletable: PropTypes.bool,
deleted: PropTypes.bool,
id: PropTypes.number,
onAddComment: PropTypes.func,
onDelete: PropTypes.func,
parentId: PropTypes.number,
projectId: PropTypes.string,
replies: PropTypes.arrayOf(PropTypes.object)

View file

@ -21,6 +21,7 @@ const StudioList = require('./studio-list.jsx');
const Subactions = require('./subactions.jsx');
const InplaceInput = require('../../components/forms/inplace-input.jsx');
const TopLevelComment = require('./comment/top-level-comment.jsx');
const ComposeComment = require('./comment/compose-comment.jsx');
const ExtensionChip = require('./extension-chip.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
@ -64,6 +65,8 @@ const PreviewPresentation = ({
projectStudios,
studios,
userOwnsProject,
onAddComment,
onDeleteComment,
onFavoriteClicked,
onLoadMore,
onLoveClicked,
@ -75,80 +78,197 @@ const PreviewPresentation = ({
onToggleStudio,
onSeeInside,
onUpdate
}) => (
<div className="preview">
<ShareBanner shared={isShared} />
{ projectInfo && projectInfo.author && projectInfo.author.id && (
<Formsy onKeyPress={onKeyPress}>
<div className="inner">
<FlexRow className="preview-row wrap-to-col">
<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: intl.formatMessage({
id: 'preview.titleMaxLength'
})
}}
validations={{
maxLength: 100
}}
value={projectInfo.title}
/> :
<React.Fragment>
<div
className="project-title no-edit"
title={projectInfo.title}
>{projectInfo.title}</div>
{'by '}
<a href={`/users/${projectInfo.author.username}`}>
{projectInfo.author.username}
</a>
</React.Fragment>
}
</div>
}) => {
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : '';
return (
<div className="preview">
<ShareBanner shared={isShared} />
{ projectInfo && projectInfo.author && projectInfo.author.id && (
<Formsy onKeyPress={onKeyPress}>
<div className="inner">
<FlexRow className="preview-row force-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: intl.formatMessage({
id: 'preview.titleMaxLength'
})
}}
validations={{
maxLength: 100
}}
value={projectInfo.title}
/> :
<React.Fragment>
<div
className="project-title no-edit"
title={projectInfo.title}
>{projectInfo.title}</div>
{'by '}
<a href={`/users/${projectInfo.author.username}`}>
{projectInfo.author.username}
</a>
</React.Fragment>
}
</div>
</FlexRow>
<MediaQuery minWidth={frameless.mobile}>
<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>
</MediaQuery>
</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
assetHost={assetHost}
backpackOptions={backpackOptions}
basePath="/"
className="guiPlayer"
isFullScreen={isFullScreen}
previewInfoVisible="false"
projectHost={projectHost}
projectId={projectId}
/>
</div>
<MediaQuery maxWidth={frameless.tablet - 1}>
<FlexRow className="preview-row force-center">
<FlexRow className="preview-row">
<div className="guiPlayer">
<IntlGUI
isPlayerOnly
assetHost={assetHost}
backpackOptions={backpackOptions}
basePath="/"
className="guiPlayer"
isFullScreen={isFullScreen}
previewInfoVisible="false"
projectHost={projectHost}
projectId={projectId}
/>
</div>
<MediaQuery maxWidth={frameless.tablet - 1}>
<FlexRow className="preview-row force-center">
<Stats
faved={faved}
favoriteCount={favoriteCount}
loveCount={loveCount}
loved={loved}
projectInfo={projectInfo}
onFavoriteClicked={onFavoriteClicked}
onLoveClicked={onLoveClicked}
/>
<Subactions
addToStudioOpen={addToStudioOpen}
isLoggedIn={isLoggedIn}
projectInfo={projectInfo}
reportOpen={reportOpen}
shareDate={shareDate}
studios={studios}
userOwnsProject={userOwnsProject}
onAddToStudioClicked={onAddToStudioClicked}
onAddToStudioClosed={onAddToStudioClosed}
onReportClicked={onReportClicked}
onReportClose={onReportClose}
onReportSubmit={onReportSubmit}
onToggleStudio={onToggleStudio}
/>
</FlexRow>
</MediaQuery>
<FlexRow className="project-notes">
<RemixCredit projectInfo={parentInfo} />
<RemixCredit projectInfo={originalInfo} />
{/* eslint-disable max-len */}
<MediaQuery maxWidth={frameless.tablet - 1}>
<FlexRow className="preview-row">
<FlexRow className="extension-list">
{extensions && extensions.map(extension => (
<ExtensionChip
extensionL10n={extension.l10nId}
extensionName={extension.name}
hasStatus={extension.hasStatus}
iconURI={extension.icon && `/svgs/project/${extension.icon}`}
key={extension.name || extension.l10nId}
/>
))}
</FlexRow>
</FlexRow>
</MediaQuery>
<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>
<MediaQuery minWidth={frameless.tablet}>
<FlexRow className="preview-row">
<Stats
faved={faved}
favoriteCount={favoriteCount}
@ -163,6 +283,7 @@ const PreviewPresentation = ({
isLoggedIn={isLoggedIn}
projectInfo={projectInfo}
reportOpen={reportOpen}
shareDate={shareDate}
studios={studios}
userOwnsProject={userOwnsProject}
onAddToStudioClicked={onAddToStudioClicked}
@ -174,176 +295,80 @@ const PreviewPresentation = ({
/>
</FlexRow>
</MediaQuery>
<FlexRow className="project-notes">
<RemixCredit projectInfo={parentInfo} />
<RemixCredit projectInfo={originalInfo} />
{/* eslint-disable max-len */}
<MediaQuery maxWidth={frameless.tablet - 1}>
<FlexRow className="preview-row">
<FlexRow className="extension-list">
{extensions && extensions.map(extension => (
<ExtensionChip
extensionL10n={extension.l10nId}
extensionName={extension.name}
hasStatus={extension.hasStatus}
iconURI={extension.icon && `/svgs/project/${extension.icon}`}
key={extension.name || extension.l10nId}
/>
))}
</FlexRow>
</FlexRow>
</MediaQuery>
<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>
<MediaQuery minWidth={frameless.tablet}>
<FlexRow className="preview-row">
<Stats
faved={faved}
favoriteCount={favoriteCount}
loveCount={loveCount}
loved={loved}
projectInfo={projectInfo}
onFavoriteClicked={onFavoriteClicked}
onLoveClicked={onLoveClicked}
/>
<Subactions
addToStudioOpen={addToStudioOpen}
isLoggedIn={isLoggedIn}
projectInfo={projectInfo}
reportOpen={reportOpen}
studios={studios}
userOwnsProject={userOwnsProject}
onAddToStudioClicked={onAddToStudioClicked}
onAddToStudioClosed={onAddToStudioClosed}
onReportClicked={onReportClicked}
onReportClose={onReportClose}
onReportSubmit={onReportSubmit}
onToggleStudio={onToggleStudio}
/>
</FlexRow>
</MediaQuery>
<MediaQuery minWidth={frameless.tablet}>
<FlexRow className="preview-row">
<FlexRow className="extension-list">
{extensions && extensions.map(extension => (
<ExtensionChip
extensionL10n={extension.l10nId}
extensionName={extension.name}
hasStatus={extension.hasStatus}
iconURI={extension.icon && `/svgs/project/${extension.icon}`}
key={extension.name || extension.l10nId}
/>
))}
</FlexRow>
</FlexRow>
</MediaQuery>
</div>
<div className="project-lower-container">
<div className="inner">
<FlexRow className="preview-row">
<div className="comments-container">
<FlexRow className="comments-header">
<h4>Comments</h4>
{/* TODO: Add toggle comments component and logic*/}
</FlexRow>
<FlexRow className="comments-list">
{comments.map(comment => (
<TopLevelComment
author={comment.author}
content={comment.content}
datetimeCreated={comment.datetime_created}
id={comment.id}
key={comment.id}
parentId={comment.parent_id}
projectId={projectId}
replies={replies && replies[comment.id] ? replies[comment.id] : []}
<MediaQuery minWidth={frameless.tablet}>
<FlexRow className="preview-row">
<FlexRow className="extension-list">
{extensions && extensions.map(extension => (
<ExtensionChip
extensionL10n={extension.l10nId}
extensionName={extension.name}
hasStatus={extension.hasStatus}
iconURI={extension.icon && `/svgs/project/${extension.icon}`}
key={extension.name || extension.l10nId}
/>
))}
{comments.length < projectInfo.stats.comments &&
</FlexRow>
</FlexRow>
</MediaQuery>
</div>
<div className="project-lower-container">
<div className="inner">
<FlexRow className="preview-row">
<div className="comments-container">
<FlexRow className="comments-header">
<h4>Comments</h4>
{/* TODO: Add toggle comments component and logic*/}
</FlexRow>
<FlexRow className="comments-root-reply">
{isLoggedIn &&
<ComposeComment
projectId={projectId}
onAddComment={onAddComment}
/>
}
</FlexRow>
<FlexRow className="comments-list">
{comments.map(comment => (
<TopLevelComment
author={comment.author}
canReply={isLoggedIn}
content={comment.content}
datetimeCreated={comment.datetime_created}
deletable={userOwnsProject}
deleted={comment.deleted}
id={comment.id}
key={comment.id}
parentId={comment.parent_id}
projectId={projectId}
replies={replies && replies[comment.id] ? replies[comment.id] : []}
onAddComment={onAddComment}
onDelete={onDeleteComment}
/>
))}
{comments.length < projectInfo.stats.comments &&
<Button
className="button load-more-button"
onClick={onLoadMore}
>
Load More
</Button>
}
}
</FlexRow>
</div>
<FlexRow className="column">
<RemixList remixes={remixes} />
<StudioList studios={projectStudios} />
</FlexRow>
</div>
<FlexRow className="column">
<RemixList remixes={remixes} />
<StudioList studios={projectStudios} />
</FlexRow>
</FlexRow>
</div>
</div>
</div>
</Formsy>
)}
</div>
);
</Formsy>
)}
</div>
);
};
PreviewPresentation.propTypes = {
addToStudioOpen: PropTypes.bool,
@ -363,8 +388,10 @@ PreviewPresentation.propTypes = {
isShared: PropTypes.bool,
loveCount: PropTypes.number,
loved: PropTypes.bool,
onAddComment: PropTypes.func,
onAddToStudioClicked: PropTypes.func,
onAddToStudioClosed: PropTypes.func,
onDeleteComment: PropTypes.func,
onFavoriteClicked: PropTypes.func,
onLoadMore: PropTypes.func,
onLoveClicked: PropTypes.func,

View file

@ -33,6 +33,8 @@ class Preview extends React.Component {
super(props);
bindAll(this, [
'addEventListeners',
'handleAddComment',
'handleDeleteComment',
'handleToggleStudio',
'handleFavoriteToggle',
'handleLoadMore',
@ -123,7 +125,8 @@ class Preview extends React.Component {
* landscape format should make the fullscreen mode active
*/
const isMobileDevice = screen.height <= frameless.mobile || screen.width <= frameless.mobile;
if (this.props.playerMode && isMobileDevice) {
const isAModalOpen = this.state.addToStudioOpen || this.state.reportOpen;
if (this.props.playerMode && isMobileDevice && !isAModalOpen) {
const isLandscape = screen.height < screen.width;
if (isLandscape) {
this.props.setFullScreen(true);
@ -164,6 +167,12 @@ class Preview extends React.Component {
});
});
}
handleAddComment (comment, topLevelCommentId) {
this.props.handleAddComment(comment, topLevelCommentId);
}
handleDeleteComment (id, topLevelCommentId) {
this.props.handleDeleteComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
}
handleReportClick () {
this.setState({reportOpen: true});
}
@ -336,8 +345,10 @@ class Preview extends React.Component {
reportOpen={this.state.reportOpen}
studios={this.props.studios}
userOwnsProject={this.props.userOwnsProject}
onAddComment={this.handleAddComment}
onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose}
onDeleteComment={this.handleDeleteComment}
onFavoriteClicked={this.handleFavoriteToggle}
onLoadMore={this.handleLoadMore}
onLoveClicked={this.handleLoveToggle}
@ -393,6 +404,8 @@ Preview.propTypes = {
getProjectStudios: PropTypes.func.isRequired,
getRemixes: PropTypes.func.isRequired,
getTopLevelComments: PropTypes.func.isRequired,
handleAddComment: PropTypes.func,
handleDeleteComment: PropTypes.func,
handleLogIn: PropTypes.func,
handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func,
@ -519,6 +532,12 @@ const mapStateToProps = state => {
};
const mapDispatchToProps = dispatch => ({
handleAddComment: (comment, topLevelCommentId) => {
dispatch(previewActions.addNewComment(comment, topLevelCommentId));
},
handleDeleteComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.deleteComment(projectId, commentId, topLevelCommentId, token));
},
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true));

View file

@ -6,12 +6,10 @@ $player-width: 482px;
$player-height: 406px;
$stage-width: 480px;
$small: "screen and (max-width : #{$mobile}-1)";
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
/* override view padding for share banner */
#view {
padding: 0;
width: 100%;
}
.gui {
@ -25,6 +23,19 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
.preview {
.inner {
margin: 0 auto;
@media #{$medium-and-smaller} {
max-width: 90%;
}
@media #{$intermediate} {
width: 90%;
min-width: 640px;
}
}
.project-title {
font-size: 1.75rem;
font-weight: 500;
@ -55,6 +66,14 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
.inplace-input {
height: calc(3rem - 4px);
}
@media #{$medium-and-smaller} {
flex-direction: row;
}
@media #{$small} {
margin-right: 0;
}
}
img {
@ -79,6 +98,10 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
text-align: left;
font-size: .8rem;
flex-grow: 1;
@media #{$medium-and-smaller} {
min-width: 100%;
}
}
.validation-message {
@ -97,7 +120,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
color: $type-white;
font-size: 1rem;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
margin-top: calc($arrow-border-width / 2);
width: 100%;
}
@ -120,7 +143,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
content: "";
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
display: none;
}
}
@ -154,7 +177,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
max-width: 100%;
flex: 1;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
padding: 0;
width: 100%;
}
@ -208,12 +231,9 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
align-items: flex-start;
flex-wrap: nowrap;
&.wrap-to-col {
@media #{$medium-and-small} {
width: 100%;
flex-wrap: wrap-reverse;
flex-direction: column;
justify-content: center;
&.force-row {
@media #{$medium-and-smaller} {
flex-direction: row;
}
}
}
@ -224,15 +244,11 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
@media #{$small} {
width: 100%;
.stage-wrapper {
max-width: 100%;
}
}
}
.force-center {
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
align-self: center;
}
}
@ -244,7 +260,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
flex: 1;
flex-flow: column;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
margin-top: 1rem;
margin-left: 0;
width: 100%;
@ -263,7 +279,11 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
padding: .5rem;
width: calc(100% - 1rem);
flex-wrap: nowrap;
align-items: flex-start;
align-items: center;
@media #{$medium-and-smaller} {
flex-direction: row;
}
}
.credit-text {
@ -367,7 +387,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
justify-content: flex-start;
flex-direction: row;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
justify-content: center;
}
@ -396,7 +416,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
/* TODO: the following can be transferred to
src/components/thumbnailcolumn/thumbnailcolumn.scss
after testing */
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
flex-direction: row;
.thumbnail {
@ -405,7 +425,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
}
}
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
margin-top: 1rem;
}
}

View file

@ -1,12 +1,10 @@
@import "../../frameless";
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
.stats {
line-height: 2rem;
justify-content: flex-start;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
margin: 0;
width: 100%;
justify-content: center;
@ -14,7 +12,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
}
& > div {
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
padding: 0 1rem;
}
}

View file

@ -7,74 +7,68 @@ const Button = require('../../components/forms/button.jsx');
const AddToStudioModal = require('../../components/modal/addtostudio/container.jsx');
const ReportModal = require('../../components/modal/report/modal.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
require('./subactions.scss');
const Subactions = props => {
const shareDate = ((props.projectInfo.history && props.projectInfo.history.shared)) ?
props.projectInfo.history.shared : '';
return (
<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">
{(props.isLoggedIn && props.userOwnsProject) &&
<React.Fragment>
<Button
className="action-button studio-button"
key="add-to-studio-button"
onClick={props.onAddToStudioClicked}
>
Add to Studio
</Button>,
<AddToStudioModal
isOpen={props.addToStudioOpen}
key="add-to-studio-modal"
studios={props.studios}
onRequestClose={props.onAddToStudioClosed}
onToggleStudio={props.onToggleStudio}
/>
</React.Fragment>
}
<Button className="action-button copy-link-button">
Copy Link
</Button>
{(props.isLoggedIn && !props.userOwnsProject) &&
const Subactions = props => (
<FlexRow className="subactions">
<div className="share-date">
<div className="copyleft">&copy;</div>
{' '}
{/* eslint-disable react/jsx-sort-props */}
{props.shareDate === null ?
'Unshared' :
<FormattedDate
value={Date.parse(props.shareDate)}
day="2-digit"
month="short"
year="numeric"
/>
}
{/* eslint-enable react/jsx-sort-props */}
</div>
<FlexRow className="action-buttons">
{(props.isLoggedIn && props.userOwnsProject) &&
<React.Fragment>
<Button
className="action-button report-button"
key="report-button"
onClick={props.onReportClicked}
className="action-button studio-button"
key="add-to-studio-button"
onClick={props.onAddToStudioClicked}
>
Report
Add to Studio
</Button>,
<ReportModal
isOpen={props.reportOpen}
key="report-modal"
type="project"
onReport={props.onReportSubmit}
onRequestClose={props.onReportClose}
<AddToStudioModal
isOpen={props.addToStudioOpen}
key="add-to-studio-modal"
studios={props.studios}
onRequestClose={props.onAddToStudioClosed}
onToggleStudio={props.onToggleStudio}
/>
</React.Fragment>
}
</FlexRow>
}
<Button className="action-button copy-link-button">
Copy Link
</Button>
{(props.isLoggedIn && !props.userOwnsProject) &&
<React.Fragment>
<Button
className="action-button report-button"
key="report-button"
onClick={props.onReportClicked}
>
Report
</Button>,
<ReportModal
isOpen={props.reportOpen}
key="report-modal"
type="project"
onReport={props.onReportSubmit}
onRequestClose={props.onReportClose}
/>
</React.Fragment>
}
</FlexRow>
);
};
</FlexRow>
);
Subactions.propTypes = {
addToStudioOpen: PropTypes.bool,
@ -85,8 +79,8 @@ Subactions.propTypes = {
onReportClose: PropTypes.func.isRequired,
onReportSubmit: PropTypes.func.isRequired,
onToggleStudio: PropTypes.func,
projectInfo: projectShape,
reportOpen: PropTypes.bool,
shareDate: PropTypes.string,
studios: PropTypes.arrayOf(PropTypes.object),
userOwnsProject: PropTypes.bool

View file

@ -1,18 +1,13 @@
@import "../../colors";
@import "../../frameless";
$small: "screen and (max-width : #{$mobile}-1)";
$medium: "screen and (min-width : #{$mobile}) and (max-width : #{$tablet}-1)";
$intermediate: "screen and (min-width : #{$tablet}) and (max-width : 941px)"; /* 941 because currently breakpoint of .inner in www is 941 */
$medium-and-small: "screen and (max-width : #{$tablet}-1)";
.subactions {
margin-left: 1.5rem;
justify-content: flex-end;
align-items: flex-start;
flex: 1;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
margin-top: 1rem;
width: 100%;
}
@ -36,7 +31,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
margin: 0;
text-align: right;
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
padding: 0;
}
}
@ -60,7 +55,7 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
line-height: .875rem;
font-size: .75rem;
font-weight: normal;
&.studio-button,
&.copy-link-button,
&.report-button {
@ -101,14 +96,14 @@ $medium-and-small: "screen and (max-width : #{$tablet}-1)";
}
.subactions, .subactions .action-buttons {
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
margin: 0;
justify-content: center;
flex-direction: row;
}
& > div, .action-button {
@media #{$medium-and-small} {
@media #{$medium-and-smaller} {
padding: 0 1rem;
}
}