Merge pull request #1985 from benjiwheeler/studio-modal-benwheeler
Add To Studio modal on project page. Sorry for merge commit, merges from develop made these un-squashable :(
|
@ -2,7 +2,6 @@ version: '3.4'
|
|||
volumes:
|
||||
npm_data:
|
||||
runtime_data:
|
||||
intl_data:
|
||||
|
||||
networks:
|
||||
scratch-api_scratch_network:
|
||||
|
@ -34,7 +33,6 @@ services:
|
|||
nocopy: true
|
||||
- npm_data:/var/app/current/node_modules
|
||||
- runtime_data:/runtime
|
||||
- intl_data:/var/app/current/intl
|
||||
ports:
|
||||
- "8333:8333"
|
||||
networks:
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
"lodash.merge": "3.3.2",
|
||||
"lodash.omit": "3.1.0",
|
||||
"lodash.range": "3.0.1",
|
||||
"lodash.truncate": "4.4.2",
|
||||
"minilog": "2.0.8",
|
||||
"node-dir": "0.1.16",
|
||||
"node-sass": "4.6.1",
|
||||
|
|
|
@ -14,7 +14,6 @@ $ui-dark-gray: hsla(0, 0, 70, 1); //#B3B3B3
|
|||
|
||||
$background-color: hsla(0, 0, 99, 1); //#FDFDFD
|
||||
|
||||
|
||||
/* UI Secondary Colors */
|
||||
/* 3.0 colors */
|
||||
/* Using www naming convention for now, should be consistent with gui */
|
||||
|
@ -28,14 +27,20 @@ $ui-coral-dark: hsla(350, 100, 60, 1); // #FF3355 More Blocks tertiary
|
|||
|
||||
$ui-white: hsla(0, 100%, 100%, 1); //#FFF
|
||||
$ui-white-15percent: hsla(0, 100%, 100%, .15); //#FFF
|
||||
$ui-light-primary: hsl(215, 100, 95);
|
||||
|
||||
$ui-border: hsla(0, 0, 85, 1); //#D9D9D9
|
||||
|
||||
/* modals */
|
||||
$ui-mint-green: hsl(163, 69, 44);
|
||||
$ui-light-mint: hsl(163, 53, 67);
|
||||
|
||||
/* Overlay UI Gray Colors */
|
||||
$active-gray: hsla(0, 0, 0, .1);
|
||||
$active-dark-gray: hsla(0, 0, 0, .2);
|
||||
$box-shadow-gray: hsla(0, 0, 0, .25);
|
||||
$overlay-gray: hsla(0, 0, 0, .75);
|
||||
$transparent-light-blue: rgba(229, 240, 254, 0);
|
||||
|
||||
/* Typography Colors */
|
||||
$header-gray: hsla(225, 15, 40, 1); //#575E75
|
||||
|
|
72
src/components/modal/addtostudio/container.jsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const AddToStudioModalPresentation = require('./presentation.jsx');
|
||||
|
||||
class AddToStudioModal extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleRequestClose',
|
||||
'handleSubmit'
|
||||
]);
|
||||
|
||||
this.state = {
|
||||
waitingToClose: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUpdate () {
|
||||
this.closeIfFinishedUpdating();
|
||||
}
|
||||
|
||||
hasOutstandingUpdates () {
|
||||
return (this.props.studios.some(studio => (studio.hasRequestOutstanding === true)));
|
||||
}
|
||||
|
||||
closeIfFinishedUpdating () {
|
||||
if (this.state.waitingToClose === true && this.hasOutstandingUpdates() === false) {
|
||||
this.closeAndStopWaiting();
|
||||
}
|
||||
}
|
||||
|
||||
// before closing, set waitingToClose to false. That way, if user reopens
|
||||
// modal, it won't unexpectedly close.
|
||||
closeAndStopWaiting () {
|
||||
this.setState({waitingToClose: false}, () => {
|
||||
this.props.onRequestClose();
|
||||
});
|
||||
}
|
||||
|
||||
handleRequestClose () {
|
||||
this.closeAndStopWaiting();
|
||||
}
|
||||
|
||||
handleSubmit () {
|
||||
this.setState({waitingToClose: true}, () => {
|
||||
this.closeIfFinishedUpdating();
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<AddToStudioModalPresentation
|
||||
isOpen={this.props.isOpen}
|
||||
studios={this.props.studios}
|
||||
waitingToClose={this.state.waitingToClose}
|
||||
onRequestClose={this.handleRequestClose}
|
||||
onSubmit={this.handleSubmit}
|
||||
onToggleStudio={this.props.onToggleStudio}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddToStudioModal.propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
onRequestClose: PropTypes.func,
|
||||
onToggleStudio: PropTypes.func,
|
||||
studios: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
module.exports = AddToStudioModal;
|
190
src/components/modal/addtostudio/modal.scss
Normal file
|
@ -0,0 +1,190 @@
|
|||
@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;
|
||||
}
|
||||
|
||||
.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;
|
||||
width: 100%;
|
||||
line-height: 1.5rem;
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.studio-list-outer-scrollbox {
|
||||
position: relative;
|
||||
background-color: $ui-blue-10percent;
|
||||
}
|
||||
|
||||
.studio-list-inner-scrollbox {
|
||||
margin-right: .5rem;
|
||||
padding-right: .5rem;
|
||||
height: 16.9375rem;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: $active-dark-gray;
|
||||
height: 92px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-list-container {
|
||||
display: flex;
|
||||
padding: .40625rem 0 0 1.46875rem;
|
||||
justify-content: flex-start;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
/* NOTE: force scrolling: add to above:
|
||||
min-height: 30rem;
|
||||
*/
|
||||
|
||||
.studio-list-bottom-gradient {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(
|
||||
$transparent-light-blue,
|
||||
$ui-light-primary
|
||||
);
|
||||
height: 32px;
|
||||
pointer-events: none; /* pass clicks through to buttons underneath */
|
||||
}
|
||||
|
||||
|
||||
.studio-selector-button {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: .21875rem .21875rem;
|
||||
border-radius: .5rem;
|
||||
background-color: $ui-white;
|
||||
padding: 0;
|
||||
width: 16.1875rem; /* 259px */
|
||||
height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.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: 1rem; /* diff from spec, in case we ever do valign to middle */
|
||||
line-height: 1.25rem;
|
||||
font-family: "Helvetica Neue";
|
||||
font-size: .875rem;
|
||||
font-weight: regular;
|
||||
}
|
||||
|
||||
.studio-selector-button-selected {
|
||||
background-color: $ui-mint-green;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
.studio-selector-button-waiting {
|
||||
background-color: $ui-light-mint;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
.studio-selector-button-text-selected {
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
.studio-selector-button-text-unselected {
|
||||
color: $type-gray;
|
||||
}
|
||||
|
||||
.studio-status-icon {
|
||||
position: absolute;
|
||||
margin: .5rem .625rem .5rem 14.0625rem;
|
||||
border-radius: .75rem;
|
||||
padding: .0625rem .075rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: $ui-white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.studio-status-icon-unselected {
|
||||
background-color: $ui-blue;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: $ui-blue;
|
||||
}
|
||||
|
||||
.submit-button-waiting {
|
||||
background-color: $ui-blue;
|
||||
}
|
||||
|
||||
.studio-status-icon-plus-img {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
}
|
||||
|
||||
.studio-status-icon--img {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
}
|
||||
|
||||
.action-button-text .spinner-smooth {
|
||||
margin: .2125rem auto;
|
||||
width: 1.875rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.studio-status-icon .spinner-smooth {
|
||||
position: unset; /* don't understand why neither relative nor absolute work */
|
||||
}
|
||||
|
||||
.studio-status-icon .spinner-smooth .circle {
|
||||
/* overlay spinner on circle */
|
||||
position: absolute;
|
||||
margin: .1875rem; /* stay within boundaries of circle */
|
||||
width: 75%; /* stay within boundaries of circle */
|
||||
height: 75%; /* stay within boundaries of circle */
|
||||
}
|
119
src/components/modal/addtostudio/presentation.jsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
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 Modal = require('../base/modal.jsx');
|
||||
|
||||
const Form = require('../../forms/form.jsx');
|
||||
const Button = require('../../forms/button.jsx');
|
||||
const Spinner = require('../../spinner/spinner.jsx');
|
||||
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||
const StudioButton = require('./studio-button.jsx');
|
||||
|
||||
require('../../forms/button.scss');
|
||||
require('./modal.scss');
|
||||
|
||||
const AddToStudioModalPresentation = ({
|
||||
intl,
|
||||
isOpen,
|
||||
studios,
|
||||
waitingToClose,
|
||||
onToggleStudio,
|
||||
onRequestClose,
|
||||
onSubmit
|
||||
}) => {
|
||||
const contentLabel = intl.formatMessage({id: 'addToStudio.title'});
|
||||
const studioButtons = studios.map(studio => (
|
||||
<StudioButton
|
||||
hasRequestOutstanding={studio.hasRequestOutstanding}
|
||||
id={studio.id}
|
||||
includesProject={studio.includesProject}
|
||||
key={studio.id}
|
||||
title={studio.title}
|
||||
onToggleStudio={onToggleStudio}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="mod-addToStudio"
|
||||
contentLabel={contentLabel}
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
>
|
||||
<div>
|
||||
<div className="addToStudio-modal-header">
|
||||
<div className="addToStudio-content-label">
|
||||
{contentLabel}
|
||||
</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}
|
||||
</div>
|
||||
<div className="studio-list-bottom-gradient" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
</Button>
|
||||
{waitingToClose ? [
|
||||
<Button
|
||||
className="action-button submit-button submit-button-waiting"
|
||||
disabled="disabled"
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
<div className="action-button-text">
|
||||
<Spinner mode="smooth" />
|
||||
<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>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
AddToStudioModalPresentation.propTypes = {
|
||||
intl: intlShape,
|
||||
isOpen: PropTypes.bool,
|
||||
onRequestClose: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onToggleStudio: PropTypes.func,
|
||||
studios: PropTypes.arrayOf(PropTypes.object),
|
||||
waitingToClose: PropTypes.bool
|
||||
};
|
||||
|
||||
module.exports = injectIntl(AddToStudioModalPresentation);
|
72
src/components/modal/addtostudio/studio-button.jsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
const truncateAtWordBoundary = require('../../../lib/truncate').truncateAtWordBoundary;
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const classNames = require('classnames');
|
||||
const Spinner = require('../../spinner/spinner.jsx');
|
||||
|
||||
require('./modal.scss');
|
||||
|
||||
const StudioButton = ({
|
||||
hasRequestOutstanding,
|
||||
id,
|
||||
includesProject,
|
||||
title,
|
||||
onToggleStudio
|
||||
}) => {
|
||||
const checkmark = (
|
||||
<img
|
||||
alt="checkmark-icon"
|
||||
className="studio-status-icon-checkmark-img"
|
||||
src="/svgs/modal/confirm.svg"
|
||||
/>
|
||||
);
|
||||
const plus = (
|
||||
<img
|
||||
alt="plus-icon"
|
||||
className="studio-status-icon-plus-img"
|
||||
src="/svgs/modal/add.svg"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'studio-selector-button',
|
||||
{'studio-selector-button-waiting': hasRequestOutstanding},
|
||||
{'studio-selector-button-selected':
|
||||
includesProject && !hasRequestOutstanding}
|
||||
)}
|
||||
data-id={id}
|
||||
onClick={onToggleStudio}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'studio-selector-button-text',
|
||||
{'studio-selector-button-text-selected': includesProject || hasRequestOutstanding},
|
||||
{'studio-selector-button-text-unselected': !includesProject && !hasRequestOutstanding}
|
||||
)}
|
||||
>
|
||||
{truncateAtWordBoundary(title, 25)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'studio-status-icon',
|
||||
{'studio-status-icon-unselected': !includesProject}
|
||||
)}
|
||||
>
|
||||
{(hasRequestOutstanding ?
|
||||
(<Spinner mode="smooth" />) :
|
||||
(includesProject ? checkmark : plus))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudioButton.propTypes = {
|
||||
hasRequestOutstanding: PropTypes.bool,
|
||||
id: PropTypes.number,
|
||||
includesProject: PropTypes.bool,
|
||||
onToggleStudio: PropTypes.func,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = StudioButton;
|
|
@ -5,10 +5,14 @@
|
|||
position: relative;
|
||||
margin: 3.75rem auto;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0 0 1px $active-gray;
|
||||
box-shadow: 0 0 0 4px $ui-white-15percent;
|
||||
background-color: $ui-white;
|
||||
padding: 0;
|
||||
width: 48.75rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
|
@ -21,10 +25,6 @@
|
|||
background-color: transparentize($ui-blue, .3);
|
||||
}
|
||||
|
||||
.modal-content:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
$modal-close-size: 1rem;
|
||||
.modal-content-close {
|
||||
position: absolute;
|
||||
|
@ -59,3 +59,27 @@ $modal-close-size: 1rem;
|
|||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Close button, Submit button, etc. */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
margin: 1.125rem .8275rem .9375rem .8275rem;
|
||||
justify-content: flex-end !important;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin: 0 0 0 .54625rem;
|
||||
border-radius: .25rem;
|
||||
padding: 6px 1.25rem 14px 1.25rem;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.action-button.close-button {
|
||||
border: 1px solid $active-gray;
|
||||
}
|
||||
|
||||
.action-button-text {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ const Button = require('../../forms/button.jsx');
|
|||
const Select = require('../../forms/select.jsx');
|
||||
const Spinner = require('../../spinner/spinner.jsx');
|
||||
const TextArea = require('../../forms/textarea.jsx');
|
||||
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||
|
||||
require('../../forms/button.scss');
|
||||
require('./modal.scss');
|
||||
|
@ -101,6 +102,10 @@ class ReportModal extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
className="report"
|
||||
onSubmit={onReport}
|
||||
>
|
||||
<div className="report-modal-content">
|
||||
<FormattedMessage
|
||||
id={`report.${type}Instructions`}
|
||||
|
@ -112,10 +117,6 @@ class ReportModal extends React.Component {
|
|||
)
|
||||
}}
|
||||
/>
|
||||
<Form
|
||||
className="report"
|
||||
onSubmit={onReport}
|
||||
>
|
||||
<Select
|
||||
required
|
||||
elementWrapperClassName="report-modal-field"
|
||||
|
@ -145,9 +146,11 @@ class ReportModal extends React.Component {
|
|||
}}
|
||||
value={report.notes}
|
||||
/>
|
||||
</div>
|
||||
<FlexRow className="action-buttons">
|
||||
{report.waiting ? [
|
||||
<Button
|
||||
className="submit-button white"
|
||||
className="submit-button"
|
||||
disabled="disabled"
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
|
@ -156,17 +159,16 @@ class ReportModal extends React.Component {
|
|||
</Button>
|
||||
] : [
|
||||
<Button
|
||||
className="submit-button white"
|
||||
className="submit-button"
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage id="report.send" />
|
||||
</Button>
|
||||
]}
|
||||
</FlexRow>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
const range = require('lodash.range');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
|
||||
require('./spinner.scss');
|
||||
|
||||
// Adapted from http://tobiasahlin.com/spinkit/
|
||||
const Spinner = () => (
|
||||
<div className="spinner">
|
||||
{range(1, 13).map(id => (
|
||||
const Spinner = ({
|
||||
mode
|
||||
}) => {
|
||||
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner');
|
||||
const spinnerDivCount = (mode === 'smooth' ? 24 : 12);
|
||||
return (
|
||||
<div className={spinnerClassName}>
|
||||
{range(1, spinnerDivCount + 1).map(id => (
|
||||
<div
|
||||
className={`circle${id} circle`}
|
||||
key={`circle${id}`}
|
||||
|
@ -14,5 +20,10 @@ const Spinner = () => (
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Spinner.propTypes = {
|
||||
mode: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = Spinner;
|
||||
|
|
|
@ -56,3 +56,63 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*********************/
|
||||
/* type === "smooth" */
|
||||
/*********************/
|
||||
|
||||
.spinner-smooth {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
animation: circleFadeDelaySmooth 1.8s infinite ease-in-out both;
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
background-color: $ui-white;
|
||||
width: 30%;
|
||||
height: 20%;
|
||||
content: "";
|
||||
|
||||
.white & {
|
||||
background-color: darken($ui-blue, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@for $i from 1 through 24 {
|
||||
$rotation: 15deg * ($i - 1);
|
||||
$delay: -1.9s + $i * .075;
|
||||
|
||||
.circle#{$i} {
|
||||
transform: rotate($rotation);
|
||||
|
||||
&:before {
|
||||
animation-delay: $delay;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes circleFadeDelaySmooth {
|
||||
0%,
|
||||
35% {
|
||||
opacity: 0;
|
||||
},
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"general.birthMonth": "Birth Month",
|
||||
"general.birthYear": "Birth Year",
|
||||
"general.donate": "Donate",
|
||||
"general.close": "Close",
|
||||
"general.collaborators": "Collaborators",
|
||||
"general.community": "Community",
|
||||
"general.confirmEmail": "Confirm Email",
|
||||
|
@ -52,6 +53,7 @@
|
|||
"general.noDeletionDescription": "Your account was scheduled for deletion but you logged in. Your account has been reactivated. If you didn’t request for your account to be deleted, you should {resetLink} to make sure your account is secure.",
|
||||
"general.noDeletionLink": "change your password",
|
||||
"general.notRequired": "Not Required",
|
||||
"general.okay": "Okay",
|
||||
"general.other": "Other",
|
||||
"general.offlineEditor": "Offline Editor",
|
||||
"general.password": "Password",
|
||||
|
|
9
src/lib/truncate.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const lodashTruncate = require('lodash.truncate');
|
||||
|
||||
/*
|
||||
* Function that applies regex for word boundaries, replaces removed string
|
||||
* with indication of ellipsis (...)
|
||||
*/
|
||||
module.exports.truncateAtWordBoundary = (str, length) => (
|
||||
lodashTruncate(str, {length: length, separator: /[.,:;]*\s+/})
|
||||
);
|
|
@ -19,7 +19,9 @@ module.exports.getInitialState = () => ({
|
|||
original: module.exports.Status.NOT_FETCHED,
|
||||
parent: module.exports.Status.NOT_FETCHED,
|
||||
remixes: module.exports.Status.NOT_FETCHED,
|
||||
studios: module.exports.Status.NOT_FETCHED
|
||||
projectStudios: module.exports.Status.NOT_FETCHED,
|
||||
curatedStudios: module.exports.Status.NOT_FETCHED,
|
||||
studioRequests: {}
|
||||
},
|
||||
projectInfo: {},
|
||||
remixes: [],
|
||||
|
@ -28,7 +30,9 @@ module.exports.getInitialState = () => ({
|
|||
loved: false,
|
||||
original: {},
|
||||
parent: {},
|
||||
studios: []
|
||||
projectStudios: [],
|
||||
curatedStudios: [],
|
||||
currentStudioIds: []
|
||||
});
|
||||
|
||||
module.exports.previewReducer = (state, action) => {
|
||||
|
@ -53,9 +57,25 @@ module.exports.previewReducer = (state, action) => {
|
|||
return Object.assign({}, state, {
|
||||
parent: action.info
|
||||
});
|
||||
case 'SET_STUDIOS':
|
||||
case 'SET_PROJECT_STUDIOS':
|
||||
// also initialize currentStudioIds, to keep track of which studios
|
||||
// the project is currently in.
|
||||
return Object.assign({}, state, {
|
||||
studios: action.items
|
||||
projectStudios: action.items,
|
||||
currentStudioIds: action.items.map(item => item.id)
|
||||
});
|
||||
case 'SET_CURATED_STUDIOS':
|
||||
return Object.assign({}, state, {curatedStudios: action.items});
|
||||
case 'ADD_PROJECT_TO_STUDIO':
|
||||
// add studio id to our studios-that-this-project-belongs-to set.
|
||||
return Object.assign({}, state, {
|
||||
currentStudioIds: state.currentStudioIds.concat(action.studioId)
|
||||
});
|
||||
case 'REMOVE_PROJECT_FROM_STUDIO':
|
||||
return Object.assign({}, state, {
|
||||
currentStudioIds: state.currentStudioIds.filter(item => (
|
||||
item !== action.studioId
|
||||
))
|
||||
});
|
||||
case 'SET_COMMENTS':
|
||||
return Object.assign({}, state, {
|
||||
|
@ -73,6 +93,10 @@ module.exports.previewReducer = (state, action) => {
|
|||
state = JSON.parse(JSON.stringify(state));
|
||||
state.status[action.infoType] = action.status;
|
||||
return state;
|
||||
case 'SET_STUDIO_FETCH_STATUS':
|
||||
state = JSON.parse(JSON.stringify(state));
|
||||
state.status.studioRequests[action.studioId] = action.status;
|
||||
return state;
|
||||
case 'ERROR':
|
||||
log.error(action.error);
|
||||
return state;
|
||||
|
@ -116,17 +140,38 @@ module.exports.setRemixes = items => ({
|
|||
items: items
|
||||
});
|
||||
|
||||
module.exports.setStudios = items => ({
|
||||
type: 'SET_STUDIOS',
|
||||
module.exports.setProjectStudios = items => ({
|
||||
type: 'SET_PROJECT_STUDIOS',
|
||||
items: items
|
||||
});
|
||||
|
||||
module.exports.setCuratedStudios = items => ({
|
||||
type: 'SET_CURATED_STUDIOS',
|
||||
items: items
|
||||
});
|
||||
|
||||
module.exports.addProjectToStudio = studioId => ({
|
||||
type: 'ADD_PROJECT_TO_STUDIO',
|
||||
studioId: studioId
|
||||
});
|
||||
|
||||
module.exports.removeProjectFromStudio = studioId => ({
|
||||
type: 'REMOVE_PROJECT_FROM_STUDIO',
|
||||
studioId: studioId
|
||||
});
|
||||
|
||||
module.exports.setFetchStatus = (type, status) => ({
|
||||
type: 'SET_FETCH_STATUS',
|
||||
infoType: type,
|
||||
status: status
|
||||
});
|
||||
|
||||
module.exports.setStudioFetchStatus = (studioId, status) => ({
|
||||
type: 'SET_STUDIO_FETCH_STATUS',
|
||||
studioId: studioId,
|
||||
status: status
|
||||
});
|
||||
|
||||
module.exports.getProjectInfo = (id, token) => (dispatch => {
|
||||
const opts = {
|
||||
uri: `/projects/${id}`
|
||||
|
@ -333,26 +378,89 @@ module.exports.getRemixes = id => (dispatch => {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports.getStudios = id => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.FETCHING));
|
||||
module.exports.getProjectStudios = id => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/projects/${id}/studios?limit=5`
|
||||
uri: `/projects/${id}/studios`
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No studios info'));
|
||||
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No projectStudios info'));
|
||||
return;
|
||||
}
|
||||
if (res.statusCode === 404) { // NotFound
|
||||
body = [];
|
||||
}
|
||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setStudios(body));
|
||||
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setProjectStudios(body));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.getCuratedStudios = username => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/users/${username}/studios/curate`
|
||||
}, (err, body, res) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No curated studios info'));
|
||||
return;
|
||||
}
|
||||
if (res.statusCode === 404) { // NotFound
|
||||
body = [];
|
||||
}
|
||||
dispatch(module.exports.setFetchStatus('curatedStudios', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setCuratedStudios(body));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.addToStudio = (studioId, projectId, token) => (dispatch => {
|
||||
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/studios/${studioId}/project/${projectId}`,
|
||||
authentication: token,
|
||||
method: 'POST'
|
||||
}, (err, body) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
dispatch(module.exports.setError('Add to studio returned no data'));
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.addProjectToStudio(studioId));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
|
||||
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/studios/${studioId}/project/${projectId}`,
|
||||
authentication: token,
|
||||
method: 'DELETE'
|
||||
}, (err, body) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
dispatch(module.exports.setError('Leave studio returned no data'));
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setStudioFetchStatus(studioId, module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.removeProjectFromStudio(studioId));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"addToStudio.title": "Add to Studio",
|
||||
"addToStudio.finishing": "Finishing up...",
|
||||
"preview.musicExtensionChip": "Music",
|
||||
"preview.penExtensionChip": "Pen",
|
||||
"preview.speechExtensionChip": "Google Speech",
|
||||
|
|
|
@ -19,6 +19,7 @@ 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 AddToStudioModal = require('../../components/modal/addtostudio/container.jsx');
|
||||
const ReportModal = require('../../components/modal/report/modal.jsx');
|
||||
const ExtensionChip = require('./extension-chip.jsx');
|
||||
|
||||
|
@ -41,6 +42,8 @@ const PreviewPresentation = ({
|
|||
projectInfo,
|
||||
remixes,
|
||||
report,
|
||||
addToStudioOpen,
|
||||
projectStudios,
|
||||
studios,
|
||||
userOwnsProject,
|
||||
onFavoriteClicked,
|
||||
|
@ -48,6 +51,9 @@ const PreviewPresentation = ({
|
|||
onReportClicked,
|
||||
onReportClose,
|
||||
onReportSubmit,
|
||||
onAddToStudioClicked,
|
||||
onAddToStudioClosed,
|
||||
onToggleStudio,
|
||||
onSeeInside,
|
||||
onUpdate
|
||||
}) => {
|
||||
|
@ -237,9 +243,24 @@ const PreviewPresentation = ({
|
|||
{/* eslint-enable react/jsx-sort-props */}
|
||||
</div>
|
||||
<FlexRow className="action-buttons">
|
||||
<Button className="action-button studio-button">
|
||||
{(isLoggedIn && userOwnsProject) &&
|
||||
<React.Fragment>
|
||||
<Button
|
||||
className="action-button studio-button"
|
||||
key="add-to-studio-button"
|
||||
onClick={onAddToStudioClicked}
|
||||
>
|
||||
Add to Studio
|
||||
</Button>
|
||||
</Button>,
|
||||
<AddToStudioModal
|
||||
isOpen={addToStudioOpen}
|
||||
key="add-to-studio-modal"
|
||||
studios={studios}
|
||||
onRequestClose={onAddToStudioClosed}
|
||||
onToggleStudio={onToggleStudio}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
<Button className="action-button copy-link-button">
|
||||
Copy Link
|
||||
</Button>
|
||||
|
@ -283,7 +304,7 @@ const PreviewPresentation = ({
|
|||
</div>
|
||||
<FlexRow className="column">
|
||||
<RemixList remixes={remixes} />
|
||||
<StudioList studios={studios} />
|
||||
<StudioList studios={projectStudios} />
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</Formsy>
|
||||
|
@ -294,6 +315,7 @@ const PreviewPresentation = ({
|
|||
};
|
||||
|
||||
PreviewPresentation.propTypes = {
|
||||
addToStudioOpen: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
extensions: PropTypes.arrayOf(PropTypes.object),
|
||||
faved: PropTypes.bool,
|
||||
|
@ -303,17 +325,21 @@ PreviewPresentation.propTypes = {
|
|||
isShared: PropTypes.bool,
|
||||
loveCount: PropTypes.number,
|
||||
loved: PropTypes.bool,
|
||||
onAddToStudioClicked: PropTypes.func,
|
||||
onAddToStudioClosed: PropTypes.func,
|
||||
onFavoriteClicked: PropTypes.func,
|
||||
onLoveClicked: PropTypes.func,
|
||||
onReportClicked: PropTypes.func.isRequired,
|
||||
onReportClose: PropTypes.func.isRequired,
|
||||
onReportSubmit: PropTypes.func.isRequired,
|
||||
onSeeInside: PropTypes.func,
|
||||
onToggleStudio: PropTypes.func,
|
||||
onUpdate: PropTypes.func,
|
||||
originalInfo: projectShape,
|
||||
parentInfo: projectShape,
|
||||
projectId: PropTypes.string,
|
||||
projectInfo: projectShape,
|
||||
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
remixes: PropTypes.arrayOf(PropTypes.object),
|
||||
report: PropTypes.shape({
|
||||
category: PropTypes.string,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// preview view can show either project page or editor page;
|
||||
// idea is that we shouldn't require a page reload to switch back and forth
|
||||
|
||||
const bindAll = require('lodash.bindall');
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
|
@ -24,6 +27,7 @@ class Preview extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'addEventListeners',
|
||||
'handleToggleStudio',
|
||||
'handleFavoriteToggle',
|
||||
'handleLoveToggle',
|
||||
'handlePermissions',
|
||||
|
@ -31,6 +35,8 @@ class Preview extends React.Component {
|
|||
'handleReportClick',
|
||||
'handleReportClose',
|
||||
'handleReportSubmit',
|
||||
'handleAddToStudioClick',
|
||||
'handleAddToStudioClose',
|
||||
'handleSeeInside',
|
||||
'handleUpdate',
|
||||
'initCounts',
|
||||
|
@ -49,6 +55,7 @@ class Preview extends React.Component {
|
|||
favoriteCount: 0,
|
||||
loveCount: 0,
|
||||
projectId: parts[1] === 'editor' ? 0 : parts[1],
|
||||
addToStudioOpen: false,
|
||||
report: {
|
||||
category: '',
|
||||
notes: '',
|
||||
|
@ -68,13 +75,14 @@ class Preview extends React.Component {
|
|||
const token = this.props.user.token;
|
||||
this.props.getProjectInfo(this.state.projectId, token);
|
||||
this.props.getRemixes(this.state.projectId, token);
|
||||
this.props.getStudios(this.state.projectId, token);
|
||||
this.props.getProjectStudios(this.state.projectId, token);
|
||||
this.props.getCuratedStudios(username);
|
||||
this.props.getFavedStatus(this.state.projectId, username, token);
|
||||
this.props.getLovedStatus(this.state.projectId, username, token);
|
||||
} else {
|
||||
this.props.getProjectInfo(this.state.projectId);
|
||||
this.props.getRemixes(this.state.projectId);
|
||||
this.props.getStudios(this.state.projectId);
|
||||
this.props.getProjectStudios(this.state.projectId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -142,6 +150,13 @@ class Preview extends React.Component {
|
|||
handleReportClose () {
|
||||
this.setState({report: {...this.state.report, open: false}});
|
||||
}
|
||||
handleAddToStudioClick () {
|
||||
this.setState({addToStudioOpen: true});
|
||||
}
|
||||
handleAddToStudioClose () {
|
||||
this.setState({addToStudioOpen: false});
|
||||
}
|
||||
// NOTE: this is a copy, change it
|
||||
handleReportSubmit (formData) {
|
||||
this.setState({report: {
|
||||
category: formData.report_category,
|
||||
|
@ -196,6 +211,22 @@ class Preview extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
handleToggleStudio (event) {
|
||||
const studioId = parseInt(event.currentTarget.dataset.id, 10);
|
||||
if (isNaN(studioId)) { // sanity check in case event had no integer data-id
|
||||
return;
|
||||
}
|
||||
const studio = this.props.studios.find(thisStudio => (thisStudio.id === studioId));
|
||||
// only send add or leave request to server if we know current status
|
||||
if ((typeof studio !== 'undefined') && ('includesProject' in studio)) {
|
||||
this.props.toggleStudio(
|
||||
(studio.includesProject === false),
|
||||
studioId,
|
||||
this.props.projectInfo.id,
|
||||
this.props.user.token
|
||||
);
|
||||
}
|
||||
}
|
||||
handleFavoriteToggle () {
|
||||
this.props.setFavedStatus(
|
||||
!this.props.faved,
|
||||
|
@ -280,6 +311,7 @@ class Preview extends React.Component {
|
|||
this.props.playerMode ?
|
||||
<Page>
|
||||
<PreviewPresentation
|
||||
addToStudioOpen={this.state.addToStudioOpen}
|
||||
comments={this.props.comments}
|
||||
editable={this.state.editable}
|
||||
extensions={this.state.extensions}
|
||||
|
@ -294,17 +326,21 @@ class Preview extends React.Component {
|
|||
parentInfo={this.props.parent}
|
||||
projectId={this.state.projectId}
|
||||
projectInfo={this.props.projectInfo}
|
||||
projectStudios={this.props.projectStudios}
|
||||
remixes={this.props.remixes}
|
||||
report={this.state.report}
|
||||
studios={this.props.studios}
|
||||
user={this.props.user}
|
||||
userOwnsProject={this.userOwnsProject()}
|
||||
onAddToStudioClicked={this.handleAddToStudioClick}
|
||||
onAddToStudioClosed={this.handleAddToStudioClose}
|
||||
onFavoriteClicked={this.handleFavoriteToggle}
|
||||
onLoveClicked={this.handleLoveToggle}
|
||||
onReportClicked={this.handleReportClick}
|
||||
onReportClose={this.handleReportClose}
|
||||
onReportSubmit={this.handleReportSubmit}
|
||||
onSeeInside={this.handleSeeInside}
|
||||
onToggleStudio={this.handleToggleStudio}
|
||||
onUpdate={this.handleUpdate}
|
||||
/>
|
||||
</Page> :
|
||||
|
@ -322,18 +358,20 @@ Preview.propTypes = {
|
|||
comments: PropTypes.arrayOf(PropTypes.object),
|
||||
faved: PropTypes.bool,
|
||||
fullScreen: PropTypes.bool,
|
||||
getCuratedStudios: PropTypes.func.isRequired,
|
||||
getFavedStatus: PropTypes.func.isRequired,
|
||||
getLovedStatus: PropTypes.func.isRequired,
|
||||
getOriginalInfo: PropTypes.func.isRequired,
|
||||
getParentInfo: PropTypes.func.isRequired,
|
||||
getProjectInfo: PropTypes.func.isRequired,
|
||||
getProjectStudios: PropTypes.func.isRequired,
|
||||
getRemixes: PropTypes.func.isRequired,
|
||||
getStudios: PropTypes.func.isRequired,
|
||||
loved: PropTypes.bool,
|
||||
original: projectShape,
|
||||
parent: projectShape,
|
||||
playerMode: PropTypes.bool,
|
||||
projectInfo: projectShape,
|
||||
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
remixes: PropTypes.arrayOf(PropTypes.object),
|
||||
sessionStatus: PropTypes.string,
|
||||
setFavedStatus: PropTypes.func.isRequired,
|
||||
|
@ -341,6 +379,7 @@ Preview.propTypes = {
|
|||
setLovedStatus: PropTypes.func.isRequired,
|
||||
setPlayer: PropTypes.func.isRequired,
|
||||
studios: PropTypes.arrayOf(PropTypes.object),
|
||||
toggleStudio: PropTypes.func.isRequired,
|
||||
updateProject: PropTypes.func.isRequired,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
|
@ -359,6 +398,40 @@ Preview.defaultProps = {
|
|||
user: {}
|
||||
};
|
||||
|
||||
// Build consolidated curatedStudios object from all studio info.
|
||||
// We add flags to indicate whether the project is currently in the studio,
|
||||
// and the status of requests to join/leave studios.
|
||||
const consolidateStudiosInfo = (curatedStudios, projectStudios, currentStudioIds, studioRequests) => {
|
||||
const consolidatedStudios = [];
|
||||
|
||||
projectStudios.forEach(projectStudio => {
|
||||
const includesProject = (currentStudioIds.indexOf(projectStudio.id) !== -1);
|
||||
const consolidatedStudio =
|
||||
Object.assign({}, projectStudio, {includesProject: includesProject});
|
||||
consolidatedStudios.push(consolidatedStudio);
|
||||
});
|
||||
|
||||
// copy the curated studios that project is not in
|
||||
curatedStudios.forEach(curatedStudio => {
|
||||
if (!projectStudios.some(projectStudio => (projectStudio.id === curatedStudio.id))) {
|
||||
const includesProject = (currentStudioIds.indexOf(curatedStudio.id) !== -1);
|
||||
const consolidatedStudio =
|
||||
Object.assign({}, curatedStudio, {includesProject: includesProject});
|
||||
consolidatedStudios.push(consolidatedStudio);
|
||||
}
|
||||
});
|
||||
|
||||
// set studio state to hasRequestOutstanding==true if it's being fetched,
|
||||
// false if it's not
|
||||
consolidatedStudios.forEach(consolidatedStudio => {
|
||||
const id = consolidatedStudio.id;
|
||||
consolidatedStudio.hasRequestOutstanding =
|
||||
((id in studioRequests) &&
|
||||
(studioRequests[id] === previewActions.Status.FETCHING));
|
||||
});
|
||||
return consolidatedStudios;
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
projectInfo: state.preview.projectInfo,
|
||||
comments: state.preview.comments,
|
||||
|
@ -368,7 +441,10 @@ const mapStateToProps = state => ({
|
|||
parent: state.preview.parent,
|
||||
remixes: state.preview.remixes,
|
||||
sessionStatus: state.session.status,
|
||||
studios: state.preview.studios,
|
||||
projectStudios: state.preview.projectStudios,
|
||||
studios: consolidateStudiosInfo(state.preview.curatedStudios,
|
||||
state.preview.projectStudios, state.preview.currentStudioIds,
|
||||
state.preview.status.studioRequests),
|
||||
user: state.session.session.user,
|
||||
playerMode: state.scratchGui.mode.isPlayerOnly,
|
||||
fullScreen: state.scratchGui.mode.isFullScreen
|
||||
|
@ -388,8 +464,18 @@ const mapDispatchToProps = dispatch => ({
|
|||
getRemixes: id => {
|
||||
dispatch(previewActions.getRemixes(id));
|
||||
},
|
||||
getStudios: id => {
|
||||
dispatch(previewActions.getStudios(id));
|
||||
getProjectStudios: id => {
|
||||
dispatch(previewActions.getProjectStudios(id));
|
||||
},
|
||||
getCuratedStudios: (username, token) => {
|
||||
dispatch(previewActions.getCuratedStudios(username, token));
|
||||
},
|
||||
toggleStudio: (isAdd, studioId, id, token) => {
|
||||
if (isAdd === true) {
|
||||
dispatch(previewActions.addToStudio(studioId, id, token));
|
||||
} else {
|
||||
dispatch(previewActions.leaveStudio(studioId, id, token));
|
||||
}
|
||||
},
|
||||
getFavedStatus: (id, username, token) => {
|
||||
dispatch(previewActions.getFavedStatus(id, username, token));
|
||||
|
|
1
static/svgs/modal/add.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 11.25h-2.5a1.25 1.25 0 1 1 0-2.5h2.5v-2.5a1.25 1.25 0 0 1 2.5 0v2.5h2.5a1.25 1.25 0 1 1 0 2.5h-2.5v2.5a1.249 1.249 0 1 1-2.5 0v-2.5z" fill="#FFF" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 251 B |
|
@ -1,17 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="16px" height="16px" viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M12.5,10.3c0.6,0.6,0.6,1.5,0,2.1c-0.3,0.3-0.7,0.4-1.1,0.4c-0.4,0-0.8-0.1-1.1-0.4L8,10.1l-2.3,2.3
|
||||
c-0.3,0.3-0.7,0.4-1.1,0.4c-0.4,0-0.8-0.1-1.1-0.4c-0.6-0.6-0.6-1.5,0-2.1L5.9,8L3.5,5.7C3,5.1,3,4.1,3.5,3.5
|
||||
C4.1,3,5.1,3,5.7,3.5L8,5.9l2.3-2.3c0.6-0.6,1.5-0.6,2.1,0c0.6,0.6,0.6,1.5,0,2.1L10.1,8L12.5,10.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M12.5 10.3c.6.6.6 1.5 0 2.1-.3.3-.7.4-1.1.4-.4 0-.8-.1-1.1-.4L8 10.1l-2.3 2.3c-.3.3-.7.4-1.1.4-.4 0-.8-.1-1.1-.4-.6-.6-.6-1.5 0-2.1L5.9 8 3.5 5.7c-.5-.6-.5-1.6 0-2.2.6-.5 1.6-.5 2.2 0L8 5.9l2.3-2.3c.6-.6 1.5-.6 2.1 0 .6.6.6 1.5 0 2.1L10.1 8l2.4 2.3z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 801 B After Width: | Height: | Size: 342 B |
1
static/svgs/modal/confirm.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path stroke="#FFF" stroke-width="2.5" d="M6 10l3 3 6-6" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
After Width: | Height: | Size: 206 B |
|
@ -1,12 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>open-blue</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="open-blue" fill="#4C97FF">
|
||||
<path d="M16.904265,9.16353035 L15.4298099,7.69520276 L10.2383662,11.6050888 C9.64794591,12.057779 8.79050666,11.9379804 8.33625534,11.3315664 C7.9692373,10.844951 7.97668404,10.1706865 8.33625534,9.70951497 L12.2606889,4.53591307 L10.8213398,3.10151073 C10.4107052,2.6922873 10.7011281,2.00848131 11.2670806,2.00848131 L17.3585163,2 C17.709577,2.00848131 18,2.29896634 18,2.64033925 L18,8.71932149 C18,9.28332892 17.3063891,9.56427246 16.904265,9.16353035 Z M15.3807332,18 L3.03722491,18 C2.46488952,18 2,17.5367082 2,16.9652796 L2,4.66419295 C2,4.09382454 2.46488952,3.62947257 3.03722491,3.62947257 L7.82335296,3.62947257 C8.39781598,3.62947257 8.86164168,4.09382454 8.86164168,4.66419295 C8.86164168,5.23562152 8.39781598,5.69891333 7.82335296,5.69891333 L4.07657745,5.69891333 L4.07657745,15.9305592 L14.3424445,15.9305592 L14.3424445,13.2101776 C14.3424445,12.638749 14.807334,12.1754572 15.3807332,12.1754572 C15.9541324,12.1754572 16.4200857,12.638749 16.4200857,13.2101776 L16.4200857,16.9652796 C16.4200857,17.5367082 15.9541324,18 15.3807332,18 Z" id="open-white"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M16.904 9.164L15.43 7.695l-5.192 3.91a1.353 1.353 0 0 1-1.902-.273 1.343 1.343 0 0 1 0-1.622l3.925-5.174-1.44-1.434a.637.637 0 0 1 .446-1.094L17.36 2a.657.657 0 0 1 .641.64v6.08c0 .563-.694.844-1.096.444zM15.381 18H3.037A1.036 1.036 0 0 1 2 16.965v-12.3c0-.571.465-1.036 1.037-1.036h4.786a1.036 1.036 0 0 1 0 2.07H4.077V15.93h10.265v-2.72c0-.572.465-1.036 1.039-1.036.573 0 1.04.464 1.04 1.035v3.755c0 .572-.467 1.035-1.04 1.035z" fill="#4C97FF" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 545 B |
|
@ -1,12 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>open-modal-icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="open-modal-icon" fill="#FFFFFF">
|
||||
<path d="M16.904265,9.16353035 L15.4298099,7.69520276 L10.2383662,11.6050888 C9.64794591,12.057779 8.79050666,11.9379804 8.33625534,11.3315664 C7.9692373,10.844951 7.97668404,10.1706865 8.33625534,9.70951497 L12.2606889,4.53591307 L10.8213398,3.10151073 C10.4107052,2.6922873 10.7011281,2.00848131 11.2670806,2.00848131 L17.3585163,2 C17.709577,2.00848131 18,2.29896634 18,2.64033925 L18,8.71932149 C18,9.28332892 17.3063891,9.56427246 16.904265,9.16353035 Z M15.3807332,18 L3.03722491,18 C2.46488952,18 2,17.5367082 2,16.9652796 L2,4.66419295 C2,4.09382454 2.46488952,3.62947257 3.03722491,3.62947257 L7.82335296,3.62947257 C8.39781598,3.62947257 8.86164168,4.09382454 8.86164168,4.66419295 C8.86164168,5.23562152 8.39781598,5.69891333 7.82335296,5.69891333 L4.07657745,5.69891333 L4.07657745,15.9305592 L14.3424445,15.9305592 L14.3424445,13.2101776 C14.3424445,12.638749 14.807334,12.1754572 15.3807332,12.1754572 C15.9541324,12.1754572 16.4200857,12.638749 16.4200857,13.2101776 L16.4200857,16.9652796 C16.4200857,17.5367082 15.9541324,18 15.3807332,18 Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M16.904 9.164L15.43 7.695l-5.192 3.91a1.353 1.353 0 0 1-1.902-.273 1.343 1.343 0 0 1 0-1.622l3.925-5.174-1.44-1.434a.637.637 0 0 1 .446-1.094L17.36 2a.657.657 0 0 1 .641.64v6.08c0 .563-.694.844-1.096.444zM15.381 18H3.037A1.036 1.036 0 0 1 2 16.965v-12.3c0-.571.465-1.036 1.037-1.036h4.786a1.036 1.036 0 0 1 0 2.07H4.077V15.93h10.265v-2.72c0-.572.465-1.036 1.039-1.036.573 0 1.04.464 1.04 1.035v3.755c0 .572-.467 1.035-1.04 1.035z" fill="#FFF" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 542 B |
1
static/svgs/modal/spinner.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M15 10a5 5 0 1 0-5 5" stroke="#FFF" stroke-width="2.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
After Width: | Height: | Size: 213 B |