Merge pull request #2924 from benjiwheeler/embed-modal

Embed/social sharing modal
This commit is contained in:
Benjamin Wheeler 2019-05-08 14:53:19 -04:00 committed by GitHub
commit d97f5b9d7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 444 additions and 15 deletions

View file

@ -0,0 +1,114 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const SocialModalPresentation = require('./presentation.jsx');
const clipboardCopy = require('clipboard-copy');
const social = require('../../../lib/social');
class SocialModal extends React.Component {
constructor (props) {
super(props);
this.embedTextarea = {};
this.embedCopyTimeoutId = null;
this.linkCopyTimeoutId = null;
this.linkTextarea = {};
this.showCopyResultTimeout = 2000;
this.state = {
showEmbedResult: false,
showLinkResult: false
};
bindAll(this, [
'handleCopyEmbed',
'handleCopyProjectLink',
'hideEmbedResult',
'hideLinkResult',
'setEmbedTextarea',
'setLinkTextarea'
]);
}
componentWillUnmount () {
this.clearEmbedCopyResultTimeout();
this.clearLinkCopyResultTimeout();
}
handleCopyEmbed () {
if (this.embedTextarea) {
this.embedTextarea.select();
clipboardCopy(this.embedTextarea.value);
if (this.state.showEmbedResult === false && this.embedCopyTimeoutId === null) {
this.setState({showEmbedResult: true}, () => {
this.embedCopyTimeoutId = setTimeout(
this.hideEmbedResult,
this.showCopyResultTimeout
);
});
}
}
}
handleCopyProjectLink () {
if (this.linkTextarea) {
this.linkTextarea.select();
clipboardCopy(this.linkTextarea.value);
if (this.state.showLinkResult === false && this.linkCopyTimeoutId === null) {
this.setState({showLinkResult: true}, () => {
this.linkCopyTimeoutId = setTimeout(
this.hideLinkResult,
this.showCopyResultTimeout
);
});
}
}
}
hideEmbedResult () {
this.setState({showEmbedResult: false});
this.embedCopyTimeoutId = null;
}
hideLinkResult () {
this.setState({showLinkResult: false});
this.linkCopyTimeoutId = null;
}
setEmbedTextarea (textarea) {
this.embedTextarea = textarea;
return textarea;
}
setLinkTextarea (textarea) {
this.linkTextarea = textarea;
return textarea;
}
clearEmbedCopyResultTimeout () {
if (this.embedCopyTimeoutId !== null) {
clearTimeout(this.embedCopyTimeoutId);
this.embedCopyTimeoutId = null;
}
}
clearLinkCopyResultTimeout () {
if (this.linkCopyTimeoutId !== null) {
clearTimeout(this.linkCopyTimeoutId);
this.linkCopyTimeoutId = null;
}
}
render () {
const projectId = this.props.projectId;
return (
<SocialModalPresentation
embedHtml={social.embedHtml(projectId)}
isOpen={this.props.isOpen}
projectUrl={social.projectUrl(projectId)}
setEmbedTextarea={this.setEmbedTextarea}
setLinkTextarea={this.setLinkTextarea}
showEmbedResult={this.state.showEmbedResult}
showLinkResult={this.state.showLinkResult}
onCopyEmbed={this.handleCopyEmbed}
onCopyProjectLink={this.handleCopyProjectLink}
onRequestClose={this.props.onRequestClose}
/>
);
}
}
SocialModal.propTypes = {
isOpen: PropTypes.bool,
onRequestClose: PropTypes.func,
projectId: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};
module.exports = SocialModal;

View file

@ -0,0 +1,132 @@
@import "../../../colors";
@import "../../../frameless";
.mod-social {
min-height: 15rem;
max-height: calc(100% - 8rem);
overflow: hidden;
}
.social-modal-header {
border-radius: 1rem 1rem 0 0;
box-shadow: inset 0 -1px 0 0 $ui-blue-dark;
background-color: $ui-blue;
}
.social-modal-content {
box-shadow: none;
width: 92%;
height: calc(100% - 0rem);
margin: 1rem auto 1.625rem;
font-size: .9375rem;
}
.social-row {
width: 100%;
margin-top: .5rem;
margin-bottom: .5rem;
align-items: start;
}
.social-spaced-row {
// width: 100%;
justify-content: space-between;
align-items: flex-end;
}
.social-row-right {
margin-left: auto;
}
.social-label-row {
width: 100%;
font-weight: bold;
margin-bottom: .5rem;
justify-content: space-between;
align-items: flex-end;
}
.social-label-title {
font-size: 1rem;
margin-right: 1.5rem;
}
.social-label-item {
margin-left: 1.5rem;
margin-right: .25rem;
}
.social-label-result {
color: $type-gray-75percent;
transition: opacity 100ms linear;
}
.link-section {
margin-top: .5rem;
}
.embed-section {
margin-top: 1rem;
}
.social-social-icon {
width: 2.75rem;
height: 2.75rem;
margin-right: .75rem;
background-repeat: no-repeat;
background-size: contain;
}
.social-twitter-icon {
background-image: url("/images/social/twitter.png");
}
.social-facebook-icon {
background-image: url("/images/social/facebook.png");
}
.social-google-classroom-icon {
background-image: url("/images/social/google-classroom.png");
}
.social-wechat-icon {
background-image: url("/images/social/wechat.png");
}
.social-form {
transition: all .2s ease;
border: 2px solid $box-shadow-light-gray;
border-radius: 8px;
background-color: $ui-blue-10percent;
color: $type-gray;
padding: .75rem .875rem;
line-height: 1.25rem;
font-size: .875rem;
box-sizing: border-box;
resize: none;
overflow: hidden;
width: 100%;
&:focus {
transition: all .2s ease;
outline: none;
border: 2px solid $ui-blue;
box-shadow: 0 0 0 4px $ui-blue-25percent;
}
&.social-textarea {
height: 6rem;
}
&.social-input {
height: 2.75rem;
}
textarea {
min-height: 4rem;
}
}
.social-hidden {
opacity: 0.0;
}

View file

@ -0,0 +1,135 @@
const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const classNames = require('classnames');
const Modal = require('../base/modal.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');
require('../../forms/button.scss');
require('./modal.scss');
const SocialModalPresentation = ({
embedHtml,
intl,
isOpen,
onCopyEmbed,
onCopyProjectLink,
onRequestClose,
projectUrl,
setEmbedTextarea,
setLinkTextarea,
showEmbedResult,
showLinkResult
}) => {
const title = intl.formatMessage({id: 'general.copyLink'});
return (
<Modal
useStandardSizes
className="mod-social"
contentLabel={title}
isOpen={isOpen}
onRequestClose={onRequestClose}
>
<div className="social-modal-header modal-header">
<div className="social-content-label content-label">
<FormattedMessage id="general.copyLink" />
</div>
</div>
<div className="modal-content social-modal-content">
{/* top row: link */}
<div className="link-section">
<FlexRow className="social-row social-spaced-row">
<FlexRow className="social-label-row">
<div className="social-label-title">
{intl.formatMessage({id: 'social.linkLabel'})}
</div>
<FlexRow className="social-spaced-row social-row-right">
<div
className={classNames(
'social-label-item',
'social-label-result',
{'social-hidden': !showLinkResult}
)}
>
{intl.formatMessage({id: 'social.embedCopiedResultText'})}
</div>
<div className="social-label-item">
<a
onClick={onCopyProjectLink}
>
{intl.formatMessage({id: 'social.copyLinkLinkText'})}
</a>
</div>
</FlexRow>
</FlexRow>
<input
readOnly
className="social-form social-input"
name="link"
ref={textarea => setLinkTextarea(textarea)}
value={projectUrl}
/>
</FlexRow>
</div>
{/* bottom row: embed */}
<div className="embed-section">
<FlexRow className="social-row social-spaced-row">
<FlexRow className="social-label-row">
<div className="social-label-title">
{intl.formatMessage({id: 'social.embedLabel'})}
</div>
<FlexRow className="social-spaced-row social-row-right">
<div
className={classNames(
'social-label-item',
'social-label-result',
{'social-hidden': !showEmbedResult}
)}
>
{intl.formatMessage({id: 'social.embedCopiedResultText'})}
</div>
<div className="social-label-item">
<a
onClick={onCopyEmbed}
>
{intl.formatMessage({id: 'social.copyEmbedLinkText'})}
</a>
</div>
</FlexRow>
</FlexRow>
<textarea
readOnly
className="social-form social-textarea"
name="embed"
ref={textarea => setEmbedTextarea(textarea)}
value={embedHtml}
/>
</FlexRow>
</div>
</div>
</Modal>
);
};
SocialModalPresentation.propTypes = {
embedHtml: PropTypes.string,
intl: intlShape,
isOpen: PropTypes.bool,
onCopyEmbed: PropTypes.func,
onCopyProjectLink: PropTypes.func,
onRequestClose: PropTypes.func,
projectUrl: PropTypes.string,
setEmbedTextarea: PropTypes.func,
setLinkTextarea: PropTypes.func,
showEmbedResult: PropTypes.bool,
showLinkResult: PropTypes.bool
};
module.exports = injectIntl(SocialModalPresentation);

View file

@ -256,5 +256,11 @@
"comments.status.suspended": "Suspended",
"comments.status.acctdel": "Account deleted",
"comments.status.deleted": "Deleted",
"comments.status.reported": "Reported"
"comments.status.reported": "Reported",
"social.embedLabel": "Embed",
"social.copyEmbedLinkText": "Copy embed",
"social.linkLabel": "Link",
"social.copyLinkLinkText": "Copy link",
"social.embedCopiedResultText": "Copied"
}

17
src/lib/social.js Normal file
View file

@ -0,0 +1,17 @@
module.exports = {};
module.exports.projectUrl = projectId => {
if (projectId) {
return `https://scratch.mit.edu/projects/${projectId}`;
}
return '';
};
module.exports.embedHtml = projectId => {
if (projectId) {
return `<iframe src="https://scratch.mit.edu/projects/${projectId}/embed" ` +
'allowtransparency="true" width="485" height="402" ' +
'frameborder="0" scrolling="no" allowfullscreen></iframe>';
}
return '';
};

View file

@ -88,7 +88,6 @@ const PreviewPresentation = ({
onAddToStudioClicked,
onAddToStudioClosed,
onCloseAdminPanel,
onCopyProjectLink,
onDeleteComment,
onFavoriteClicked,
onGreenFlag,
@ -108,6 +107,8 @@ const PreviewPresentation = ({
onSeeInside,
onSetProjectThumbnailer,
onShare,
onSocialClicked,
onSocialClosed,
onToggleComments,
onToggleStudio,
onUpdateProjectId,
@ -126,6 +127,7 @@ const PreviewPresentation = ({
showAdminPanel,
showModInfo,
singleCommentId,
socialOpen,
userOwnsProject,
visibilityInfo
}) => {
@ -368,13 +370,15 @@ const PreviewPresentation = ({
projectInfo={projectInfo}
reportOpen={reportOpen}
shareDate={shareDate}
socialOpen={socialOpen}
userOwnsProject={userOwnsProject}
onAddToStudioClicked={onAddToStudioClicked}
onAddToStudioClosed={onAddToStudioClosed}
onCopyProjectLink={onCopyProjectLink}
onReportClicked={onReportClicked}
onReportClose={onReportClose}
onReportSubmit={onReportSubmit}
onSocialClicked={onSocialClicked}
onSocialClosed={onSocialClosed}
onToggleStudio={onToggleStudio}
/>
</div>
@ -514,13 +518,15 @@ const PreviewPresentation = ({
projectInfo={projectInfo}
reportOpen={reportOpen}
shareDate={shareDate}
socialOpen={socialOpen}
userOwnsProject={userOwnsProject}
onAddToStudioClicked={onAddToStudioClicked}
onAddToStudioClosed={onAddToStudioClosed}
onCopyProjectLink={onCopyProjectLink}
onReportClicked={onReportClicked}
onReportClose={onReportClose}
onReportSubmit={onReportSubmit}
onSocialClicked={onSocialClicked}
onSocialClosed={onSocialClosed}
onToggleStudio={onToggleStudio}
/>
</FlexRow>
@ -693,7 +699,6 @@ PreviewPresentation.propTypes = {
onAddToStudioClicked: PropTypes.func,
onAddToStudioClosed: PropTypes.func,
onCloseAdminPanel: PropTypes.func,
onCopyProjectLink: PropTypes.func,
onDeleteComment: PropTypes.func,
onFavoriteClicked: PropTypes.func,
onGreenFlag: PropTypes.func,
@ -713,6 +718,8 @@ PreviewPresentation.propTypes = {
onSeeInside: PropTypes.func,
onSetProjectThumbnailer: PropTypes.func,
onShare: PropTypes.func,
onSocialClicked: PropTypes.func,
onSocialClosed: PropTypes.func,
onToggleComments: PropTypes.func,
onToggleStudio: PropTypes.func,
onUpdateProjectId: PropTypes.func,
@ -731,6 +738,7 @@ PreviewPresentation.propTypes = {
showModInfo: PropTypes.bool,
showUsernameBlockAlert: PropTypes.bool,
singleCommentId: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
socialOpen: PropTypes.bool,
userOwnsProject: PropTypes.bool,
visibilityInfo: PropTypes.shape({
censored: PropTypes.bool,

View file

@ -8,7 +8,6 @@ const PropTypes = require('prop-types');
const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl;
const parser = require('scratch-parser');
const copy = require('clipboard-copy');
const Page = require('../../components/page/www/page.jsx');
const storage = require('../../lib/storage.js').default;
@ -55,8 +54,9 @@ class Preview extends React.Component {
'fetchCommunityData',
'handleAddComment',
'handleClickLogo',
'handleCopyProjectLink',
'handleDeleteComment',
'handleSocialClick',
'handleSocialClose',
'handleToggleStudio',
'handleFavoriteToggle',
'handleLoadMore',
@ -109,6 +109,7 @@ class Preview extends React.Component {
clientFaved: false,
clientLoved: false,
extensions: [],
socialOpen: false,
favoriteCount: 0,
isProjectLoaded: false,
isRemixing: false,
@ -381,6 +382,12 @@ class Preview extends React.Component {
handleAddToStudioClose () {
this.setState({addToStudioOpen: false});
}
handleSocialClick () {
this.setState({socialOpen: true});
}
handleSocialClose () {
this.setState({socialOpen: false});
}
handleReportSubmit (formData) {
const submit = data => this.props.reportProject(this.state.projectId, data, this.props.user.token);
if (this.getProjectThumbnail) {
@ -579,11 +586,6 @@ class Preview extends React.Component {
this.props.user.token
);
}
handleCopyProjectLink () {
// Use the pathname so we do not have to update this if path changes
// Also do not include hash or query params
copy(`${window.location.origin}${window.location.pathname}`);
}
initCounts (favorites, loves) {
this.setState({
favoriteCount: favorites,
@ -678,13 +680,13 @@ class Preview extends React.Component {
showModInfo={this.props.isAdmin}
showUsernameBlockAlert={this.state.showUsernameBlockAlert}
singleCommentId={this.state.singleCommentId}
socialOpen={this.state.socialOpen}
userOwnsProject={this.props.userOwnsProject}
visibilityInfo={this.props.visibilityInfo}
onAddComment={this.handleAddComment}
onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose}
onCloseAdminPanel={this.handleCloseAdminPanel}
onCopyProjectLink={this.handleCopyProjectLink}
onDeleteComment={this.handleDeleteComment}
onFavoriteClicked={this.handleFavoriteToggle}
onGreenFlag={this.handleGreenFlag}
@ -704,6 +706,8 @@ class Preview extends React.Component {
onSeeInside={this.handleSeeInside}
onSetProjectThumbnailer={this.handleSetProjectThumbnailer}
onShare={this.handleShare}
onSocialClicked={this.handleSocialClick}
onSocialClosed={this.handleSocialClose}
onToggleComments={this.handleToggleComments}
onToggleStudio={this.handleToggleStudio}
onUpdateProjectId={this.handleUpdateProjectId}

View file

@ -6,7 +6,9 @@ const FlexRow = require('../../components/flex-row/flex-row.jsx');
const Button = require('../../components/forms/button.jsx');
const AddToStudioModal = require('./add-to-studio.jsx');
const SocialModal = require('../../components/modal/social/container.jsx');
const ReportModal = require('../../components/modal/report/modal.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
require('./subactions.scss');
@ -50,10 +52,18 @@ const Subactions = props => (
}
<Button
className="action-button copy-link-button"
onClick={props.onCopyProjectLink}
onClick={props.onSocialClicked}
>
<FormattedMessage id="general.copyLink" />
</Button>
{props.socialOpen && props.projectInfo && props.projectInfo.id && (
<SocialModal
isOpen
key="social-modal"
projectId={props.projectInfo && props.projectInfo.id}
onRequestClose={props.onSocialClosed}
/>
)}
{(props.canReport) &&
<React.Fragment>
<Button
@ -85,13 +95,16 @@ Subactions.propTypes = {
isAdmin: PropTypes.bool,
onAddToStudioClicked: PropTypes.func,
onAddToStudioClosed: PropTypes.func,
onCopyProjectLink: PropTypes.func,
onReportClicked: PropTypes.func.isRequired,
onReportClose: PropTypes.func.isRequired,
onReportSubmit: PropTypes.func.isRequired,
onSocialClicked: PropTypes.func,
onSocialClosed: PropTypes.func,
onToggleStudio: PropTypes.func,
projectInfo: projectShape,
reportOpen: PropTypes.bool,
shareDate: PropTypes.string,
socialOpen: PropTypes.bool,
userOwnsProject: PropTypes.bool
};