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
Makefile
|
@ -70,7 +70,7 @@ integration:
|
||||||
|
|
||||||
smoke:
|
smoke:
|
||||||
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600
|
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600
|
||||||
|
|
||||||
smoke-verbose:
|
smoke-verbose:
|
||||||
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 -R spec
|
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 -R spec
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ version: '3.4'
|
||||||
volumes:
|
volumes:
|
||||||
npm_data:
|
npm_data:
|
||||||
runtime_data:
|
runtime_data:
|
||||||
intl_data:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
scratch-api_scratch_network:
|
scratch-api_scratch_network:
|
||||||
|
@ -16,7 +15,7 @@ services:
|
||||||
- API_HOST=http://localhost:8491
|
- API_HOST=http://localhost:8491
|
||||||
- FALLBACK=http://localhost:8080
|
- FALLBACK=http://localhost:8080
|
||||||
- USE_DOCKER_WATCHOPTIONS=true
|
- USE_DOCKER_WATCHOPTIONS=true
|
||||||
build:
|
build:
|
||||||
context: ./
|
context: ./
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: scratch-www:latest
|
image: scratch-www:latest
|
||||||
|
@ -34,7 +33,6 @@ services:
|
||||||
nocopy: true
|
nocopy: true
|
||||||
- npm_data:/var/app/current/node_modules
|
- npm_data:/var/app/current/node_modules
|
||||||
- runtime_data:/runtime
|
- runtime_data:/runtime
|
||||||
- intl_data:/var/app/current/intl
|
|
||||||
ports:
|
ports:
|
||||||
- "8333:8333"
|
- "8333:8333"
|
||||||
networks:
|
networks:
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
"lodash.merge": "3.3.2",
|
"lodash.merge": "3.3.2",
|
||||||
"lodash.omit": "3.1.0",
|
"lodash.omit": "3.1.0",
|
||||||
"lodash.range": "3.0.1",
|
"lodash.range": "3.0.1",
|
||||||
|
"lodash.truncate": "4.4.2",
|
||||||
"minilog": "2.0.8",
|
"minilog": "2.0.8",
|
||||||
"node-dir": "0.1.16",
|
"node-dir": "0.1.16",
|
||||||
"node-sass": "4.6.1",
|
"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
|
$background-color: hsla(0, 0, 99, 1); //#FDFDFD
|
||||||
|
|
||||||
|
|
||||||
/* UI Secondary Colors */
|
/* UI Secondary Colors */
|
||||||
/* 3.0 colors */
|
/* 3.0 colors */
|
||||||
/* Using www naming convention for now, should be consistent with gui */
|
/* 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: hsla(0, 100%, 100%, 1); //#FFF
|
||||||
$ui-white-15percent: hsla(0, 100%, 100%, .15); //#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
|
$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 */
|
/* Overlay UI Gray Colors */
|
||||||
$active-gray: hsla(0, 0, 0, .1);
|
$active-gray: hsla(0, 0, 0, .1);
|
||||||
$active-dark-gray: hsla(0, 0, 0, .2);
|
$active-dark-gray: hsla(0, 0, 0, .2);
|
||||||
$box-shadow-gray: hsla(0, 0, 0, .25);
|
$box-shadow-gray: hsla(0, 0, 0, .25);
|
||||||
$overlay-gray: hsla(0, 0, 0, .75);
|
$overlay-gray: hsla(0, 0, 0, .75);
|
||||||
|
$transparent-light-blue: rgba(229, 240, 254, 0);
|
||||||
|
|
||||||
/* Typography Colors */
|
/* Typography Colors */
|
||||||
$header-gray: hsla(225, 15, 40, 1); //#575E75
|
$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;
|
position: relative;
|
||||||
margin: 3.75rem auto;
|
margin: 3.75rem auto;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
box-shadow: 0 0 0 1px $active-gray;
|
box-shadow: 0 0 0 4px $ui-white-15percent;
|
||||||
background-color: $ui-white;
|
background-color: $ui-white;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 48.75rem;
|
width: 48.75rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
|
@ -21,10 +25,6 @@
|
||||||
background-color: transparentize($ui-blue, .3);
|
background-color: transparentize($ui-blue, .3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
$modal-close-size: 1rem;
|
$modal-close-size: 1rem;
|
||||||
.modal-content-close {
|
.modal-content-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -59,3 +59,27 @@ $modal-close-size: 1rem;
|
||||||
position: fixed;
|
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 Select = require('../../forms/select.jsx');
|
||||||
const Spinner = require('../../spinner/spinner.jsx');
|
const Spinner = require('../../spinner/spinner.jsx');
|
||||||
const TextArea = require('../../forms/textarea.jsx');
|
const TextArea = require('../../forms/textarea.jsx');
|
||||||
|
const FlexRow = require('../../flex-row/flex-row.jsx');
|
||||||
|
|
||||||
require('../../forms/button.scss');
|
require('../../forms/button.scss');
|
||||||
require('./modal.scss');
|
require('./modal.scss');
|
||||||
|
@ -101,21 +102,21 @@ class ReportModal extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="report-modal-content">
|
<Form
|
||||||
<FormattedMessage
|
className="report"
|
||||||
id={`report.${type}Instructions`}
|
onSubmit={onReport}
|
||||||
values={{
|
>
|
||||||
CommunityGuidelinesLink: (
|
<div className="report-modal-content">
|
||||||
<a href="/community_guidelines">
|
<FormattedMessage
|
||||||
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
|
id={`report.${type}Instructions`}
|
||||||
</a>
|
values={{
|
||||||
)
|
CommunityGuidelinesLink: (
|
||||||
}}
|
<a href="/community_guidelines">
|
||||||
/>
|
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
|
||||||
<Form
|
</a>
|
||||||
className="report"
|
)
|
||||||
onSubmit={onReport}
|
}}
|
||||||
>
|
/>
|
||||||
<Select
|
<Select
|
||||||
required
|
required
|
||||||
elementWrapperClassName="report-modal-field"
|
elementWrapperClassName="report-modal-field"
|
||||||
|
@ -145,9 +146,11 @@ class ReportModal extends React.Component {
|
||||||
}}
|
}}
|
||||||
value={report.notes}
|
value={report.notes}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<FlexRow className="action-buttons">
|
||||||
{report.waiting ? [
|
{report.waiting ? [
|
||||||
<Button
|
<Button
|
||||||
className="submit-button white"
|
className="submit-button"
|
||||||
disabled="disabled"
|
disabled="disabled"
|
||||||
key="submitButton"
|
key="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -156,17 +159,16 @@ class ReportModal extends React.Component {
|
||||||
</Button>
|
</Button>
|
||||||
] : [
|
] : [
|
||||||
<Button
|
<Button
|
||||||
className="submit-button white"
|
className="submit-button"
|
||||||
key="submitButton"
|
key="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<FormattedMessage id="report.send" />
|
<FormattedMessage id="report.send" />
|
||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
</Form>
|
</FlexRow>
|
||||||
</div>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
width: 80%;
|
width: 80%;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
|
|
||||||
.validation-message {
|
.validation-message {
|
||||||
$arrow-border-width: 1rem;
|
$arrow-border-width: 1rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -1,18 +1,29 @@
|
||||||
const range = require('lodash.range');
|
const range = require('lodash.range');
|
||||||
|
const PropTypes = require('prop-types');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
require('./spinner.scss');
|
require('./spinner.scss');
|
||||||
|
|
||||||
// Adapted from http://tobiasahlin.com/spinkit/
|
// Adapted from http://tobiasahlin.com/spinkit/
|
||||||
const Spinner = () => (
|
const Spinner = ({
|
||||||
<div className="spinner">
|
mode
|
||||||
{range(1, 13).map(id => (
|
}) => {
|
||||||
<div
|
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner');
|
||||||
className={`circle${id} circle`}
|
const spinnerDivCount = (mode === 'smooth' ? 24 : 12);
|
||||||
key={`circle${id}`}
|
return (
|
||||||
/>
|
<div className={spinnerClassName}>
|
||||||
))}
|
{range(1, spinnerDivCount + 1).map(id => (
|
||||||
</div>
|
<div
|
||||||
);
|
className={`circle${id} circle`}
|
||||||
|
key={`circle${id}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Spinner.propTypes = {
|
||||||
|
mode: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = Spinner;
|
module.exports = Spinner;
|
||||||
|
|
|
@ -56,3 +56,63 @@
|
||||||
opacity: 1;
|
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.birthMonth": "Birth Month",
|
||||||
"general.birthYear": "Birth Year",
|
"general.birthYear": "Birth Year",
|
||||||
"general.donate": "Donate",
|
"general.donate": "Donate",
|
||||||
|
"general.close": "Close",
|
||||||
"general.collaborators": "Collaborators",
|
"general.collaborators": "Collaborators",
|
||||||
"general.community": "Community",
|
"general.community": "Community",
|
||||||
"general.confirmEmail": "Confirm Email",
|
"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.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.noDeletionLink": "change your password",
|
||||||
"general.notRequired": "Not Required",
|
"general.notRequired": "Not Required",
|
||||||
|
"general.okay": "Okay",
|
||||||
"general.other": "Other",
|
"general.other": "Other",
|
||||||
"general.offlineEditor": "Offline Editor",
|
"general.offlineEditor": "Offline Editor",
|
||||||
"general.password": "Password",
|
"general.password": "Password",
|
||||||
|
@ -193,4 +195,4 @@
|
||||||
"report.tooLongError": "That's too long! Please find a way to shorten your text.",
|
"report.tooLongError": "That's too long! Please find a way to shorten your text.",
|
||||||
"report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
|
"report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
|
||||||
"report.send": "Send"
|
"report.send": "Send"
|
||||||
}
|
}
|
||||||
|
|
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,
|
original: module.exports.Status.NOT_FETCHED,
|
||||||
parent: module.exports.Status.NOT_FETCHED,
|
parent: module.exports.Status.NOT_FETCHED,
|
||||||
remixes: 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: {},
|
projectInfo: {},
|
||||||
remixes: [],
|
remixes: [],
|
||||||
|
@ -28,7 +30,9 @@ module.exports.getInitialState = () => ({
|
||||||
loved: false,
|
loved: false,
|
||||||
original: {},
|
original: {},
|
||||||
parent: {},
|
parent: {},
|
||||||
studios: []
|
projectStudios: [],
|
||||||
|
curatedStudios: [],
|
||||||
|
currentStudioIds: []
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.previewReducer = (state, action) => {
|
module.exports.previewReducer = (state, action) => {
|
||||||
|
@ -53,9 +57,25 @@ module.exports.previewReducer = (state, action) => {
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
parent: action.info
|
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, {
|
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':
|
case 'SET_COMMENTS':
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
|
@ -73,6 +93,10 @@ module.exports.previewReducer = (state, action) => {
|
||||||
state = JSON.parse(JSON.stringify(state));
|
state = JSON.parse(JSON.stringify(state));
|
||||||
state.status[action.infoType] = action.status;
|
state.status[action.infoType] = action.status;
|
||||||
return state;
|
return state;
|
||||||
|
case 'SET_STUDIO_FETCH_STATUS':
|
||||||
|
state = JSON.parse(JSON.stringify(state));
|
||||||
|
state.status.studioRequests[action.studioId] = action.status;
|
||||||
|
return state;
|
||||||
case 'ERROR':
|
case 'ERROR':
|
||||||
log.error(action.error);
|
log.error(action.error);
|
||||||
return state;
|
return state;
|
||||||
|
@ -116,17 +140,38 @@ module.exports.setRemixes = items => ({
|
||||||
items: items
|
items: items
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.setStudios = items => ({
|
module.exports.setProjectStudios = items => ({
|
||||||
type: 'SET_STUDIOS',
|
type: 'SET_PROJECT_STUDIOS',
|
||||||
items: items
|
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) => ({
|
module.exports.setFetchStatus = (type, status) => ({
|
||||||
type: 'SET_FETCH_STATUS',
|
type: 'SET_FETCH_STATUS',
|
||||||
infoType: type,
|
infoType: type,
|
||||||
status: status
|
status: status
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports.setStudioFetchStatus = (studioId, status) => ({
|
||||||
|
type: 'SET_STUDIO_FETCH_STATUS',
|
||||||
|
studioId: studioId,
|
||||||
|
status: status
|
||||||
|
});
|
||||||
|
|
||||||
module.exports.getProjectInfo = (id, token) => (dispatch => {
|
module.exports.getProjectInfo = (id, token) => (dispatch => {
|
||||||
const opts = {
|
const opts = {
|
||||||
uri: `/projects/${id}`
|
uri: `/projects/${id}`
|
||||||
|
@ -333,26 +378,89 @@ module.exports.getRemixes = id => (dispatch => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.getStudios = id => (dispatch => {
|
module.exports.getProjectStudios = id => (dispatch => {
|
||||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.FETCHING));
|
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.FETCHING));
|
||||||
api({
|
api({
|
||||||
uri: `/projects/${id}/studios?limit=5`
|
uri: `/projects/${id}/studios`
|
||||||
}, (err, body, res) => {
|
}, (err, body, res) => {
|
||||||
if (err) {
|
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));
|
dispatch(module.exports.setError(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof body === 'undefined') {
|
if (typeof body === 'undefined') {
|
||||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.ERROR));
|
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.ERROR));
|
||||||
dispatch(module.exports.setError('No studios info'));
|
dispatch(module.exports.setError('No projectStudios info'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res.statusCode === 404) { // NotFound
|
if (res.statusCode === 404) { // NotFound
|
||||||
body = [];
|
body = [];
|
||||||
}
|
}
|
||||||
dispatch(module.exports.setFetchStatus('studios', module.exports.Status.FETCHED));
|
dispatch(module.exports.setFetchStatus('projectStudios', module.exports.Status.FETCHED));
|
||||||
dispatch(module.exports.setStudios(body));
|
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,7 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"addToStudio.title": "Add to Studio",
|
||||||
|
"addToStudio.finishing": "Finishing up...",
|
||||||
"preview.musicExtensionChip": "Music",
|
"preview.musicExtensionChip": "Music",
|
||||||
"preview.penExtensionChip": "Pen",
|
"preview.penExtensionChip": "Pen",
|
||||||
"preview.speechExtensionChip": "Google Speech",
|
"preview.speechExtensionChip": "Google Speech",
|
||||||
"preview.translateExtensionChip": "Google Translate",
|
"preview.translateExtensionChip": "Google Translate",
|
||||||
"preview.videoMotionChip": "Video Motion"
|
"preview.videoMotionChip": "Video Motion"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ const RemixCredit = require('./remix-credit.jsx');
|
||||||
const RemixList = require('./remix-list.jsx');
|
const RemixList = require('./remix-list.jsx');
|
||||||
const StudioList = require('./studio-list.jsx');
|
const StudioList = require('./studio-list.jsx');
|
||||||
const InplaceInput = require('../../components/forms/inplace-input.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 ReportModal = require('../../components/modal/report/modal.jsx');
|
||||||
const ExtensionChip = require('./extension-chip.jsx');
|
const ExtensionChip = require('./extension-chip.jsx');
|
||||||
|
|
||||||
|
@ -41,6 +42,8 @@ const PreviewPresentation = ({
|
||||||
projectInfo,
|
projectInfo,
|
||||||
remixes,
|
remixes,
|
||||||
report,
|
report,
|
||||||
|
addToStudioOpen,
|
||||||
|
projectStudios,
|
||||||
studios,
|
studios,
|
||||||
userOwnsProject,
|
userOwnsProject,
|
||||||
onFavoriteClicked,
|
onFavoriteClicked,
|
||||||
|
@ -48,6 +51,9 @@ const PreviewPresentation = ({
|
||||||
onReportClicked,
|
onReportClicked,
|
||||||
onReportClose,
|
onReportClose,
|
||||||
onReportSubmit,
|
onReportSubmit,
|
||||||
|
onAddToStudioClicked,
|
||||||
|
onAddToStudioClosed,
|
||||||
|
onToggleStudio,
|
||||||
onSeeInside,
|
onSeeInside,
|
||||||
onUpdate
|
onUpdate
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -55,7 +61,7 @@ const PreviewPresentation = ({
|
||||||
return (
|
return (
|
||||||
<div className="preview">
|
<div className="preview">
|
||||||
<ShareBanner shared={isShared} />
|
<ShareBanner shared={isShared} />
|
||||||
|
|
||||||
{ projectInfo && projectInfo.author && projectInfo.author.id && (
|
{ projectInfo && projectInfo.author && projectInfo.author.id && (
|
||||||
<div className="inner">
|
<div className="inner">
|
||||||
<Formsy>
|
<Formsy>
|
||||||
|
@ -69,7 +75,7 @@ const PreviewPresentation = ({
|
||||||
</a>
|
</a>
|
||||||
<div className="title">
|
<div className="title">
|
||||||
{editable ?
|
{editable ?
|
||||||
|
|
||||||
<InplaceInput
|
<InplaceInput
|
||||||
className="project-title"
|
className="project-title"
|
||||||
handleUpdate={onUpdate}
|
handleUpdate={onUpdate}
|
||||||
|
@ -237,9 +243,24 @@ const PreviewPresentation = ({
|
||||||
{/* eslint-enable react/jsx-sort-props */}
|
{/* eslint-enable react/jsx-sort-props */}
|
||||||
</div>
|
</div>
|
||||||
<FlexRow className="action-buttons">
|
<FlexRow className="action-buttons">
|
||||||
<Button className="action-button studio-button">
|
{(isLoggedIn && userOwnsProject) &&
|
||||||
Add to Studio
|
<React.Fragment>
|
||||||
</Button>
|
<Button
|
||||||
|
className="action-button studio-button"
|
||||||
|
key="add-to-studio-button"
|
||||||
|
onClick={onAddToStudioClicked}
|
||||||
|
>
|
||||||
|
Add to Studio
|
||||||
|
</Button>,
|
||||||
|
<AddToStudioModal
|
||||||
|
isOpen={addToStudioOpen}
|
||||||
|
key="add-to-studio-modal"
|
||||||
|
studios={studios}
|
||||||
|
onRequestClose={onAddToStudioClosed}
|
||||||
|
onToggleStudio={onToggleStudio}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
<Button className="action-button copy-link-button">
|
<Button className="action-button copy-link-button">
|
||||||
Copy Link
|
Copy Link
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -283,7 +304,7 @@ const PreviewPresentation = ({
|
||||||
</div>
|
</div>
|
||||||
<FlexRow className="column">
|
<FlexRow className="column">
|
||||||
<RemixList remixes={remixes} />
|
<RemixList remixes={remixes} />
|
||||||
<StudioList studios={studios} />
|
<StudioList studios={projectStudios} />
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
</Formsy>
|
</Formsy>
|
||||||
|
@ -294,6 +315,7 @@ const PreviewPresentation = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
PreviewPresentation.propTypes = {
|
PreviewPresentation.propTypes = {
|
||||||
|
addToStudioOpen: PropTypes.bool,
|
||||||
editable: PropTypes.bool,
|
editable: PropTypes.bool,
|
||||||
extensions: PropTypes.arrayOf(PropTypes.object),
|
extensions: PropTypes.arrayOf(PropTypes.object),
|
||||||
faved: PropTypes.bool,
|
faved: PropTypes.bool,
|
||||||
|
@ -303,17 +325,21 @@ PreviewPresentation.propTypes = {
|
||||||
isShared: PropTypes.bool,
|
isShared: PropTypes.bool,
|
||||||
loveCount: PropTypes.number,
|
loveCount: PropTypes.number,
|
||||||
loved: PropTypes.bool,
|
loved: PropTypes.bool,
|
||||||
|
onAddToStudioClicked: PropTypes.func,
|
||||||
|
onAddToStudioClosed: PropTypes.func,
|
||||||
onFavoriteClicked: PropTypes.func,
|
onFavoriteClicked: PropTypes.func,
|
||||||
onLoveClicked: PropTypes.func,
|
onLoveClicked: PropTypes.func,
|
||||||
onReportClicked: PropTypes.func.isRequired,
|
onReportClicked: PropTypes.func.isRequired,
|
||||||
onReportClose: PropTypes.func.isRequired,
|
onReportClose: PropTypes.func.isRequired,
|
||||||
onReportSubmit: PropTypes.func.isRequired,
|
onReportSubmit: PropTypes.func.isRequired,
|
||||||
onSeeInside: PropTypes.func,
|
onSeeInside: PropTypes.func,
|
||||||
|
onToggleStudio: PropTypes.func,
|
||||||
onUpdate: PropTypes.func,
|
onUpdate: PropTypes.func,
|
||||||
originalInfo: projectShape,
|
originalInfo: projectShape,
|
||||||
parentInfo: projectShape,
|
parentInfo: projectShape,
|
||||||
projectId: PropTypes.string,
|
projectId: PropTypes.string,
|
||||||
projectInfo: projectShape,
|
projectInfo: projectShape,
|
||||||
|
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||||
remixes: PropTypes.arrayOf(PropTypes.object),
|
remixes: PropTypes.arrayOf(PropTypes.object),
|
||||||
report: PropTypes.shape({
|
report: PropTypes.shape({
|
||||||
category: PropTypes.string,
|
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 bindAll = require('lodash.bindall');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
|
@ -24,6 +27,7 @@ class Preview extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'addEventListeners',
|
'addEventListeners',
|
||||||
|
'handleToggleStudio',
|
||||||
'handleFavoriteToggle',
|
'handleFavoriteToggle',
|
||||||
'handleLoveToggle',
|
'handleLoveToggle',
|
||||||
'handlePermissions',
|
'handlePermissions',
|
||||||
|
@ -31,6 +35,8 @@ class Preview extends React.Component {
|
||||||
'handleReportClick',
|
'handleReportClick',
|
||||||
'handleReportClose',
|
'handleReportClose',
|
||||||
'handleReportSubmit',
|
'handleReportSubmit',
|
||||||
|
'handleAddToStudioClick',
|
||||||
|
'handleAddToStudioClose',
|
||||||
'handleSeeInside',
|
'handleSeeInside',
|
||||||
'handleUpdate',
|
'handleUpdate',
|
||||||
'initCounts',
|
'initCounts',
|
||||||
|
@ -49,6 +55,7 @@ class Preview extends React.Component {
|
||||||
favoriteCount: 0,
|
favoriteCount: 0,
|
||||||
loveCount: 0,
|
loveCount: 0,
|
||||||
projectId: parts[1] === 'editor' ? 0 : parts[1],
|
projectId: parts[1] === 'editor' ? 0 : parts[1],
|
||||||
|
addToStudioOpen: false,
|
||||||
report: {
|
report: {
|
||||||
category: '',
|
category: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
|
@ -68,15 +75,16 @@ class Preview extends React.Component {
|
||||||
const token = this.props.user.token;
|
const token = this.props.user.token;
|
||||||
this.props.getProjectInfo(this.state.projectId, token);
|
this.props.getProjectInfo(this.state.projectId, token);
|
||||||
this.props.getRemixes(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.getFavedStatus(this.state.projectId, username, token);
|
||||||
this.props.getLovedStatus(this.state.projectId, username, token);
|
this.props.getLovedStatus(this.state.projectId, username, token);
|
||||||
} else {
|
} else {
|
||||||
this.props.getProjectInfo(this.state.projectId);
|
this.props.getProjectInfo(this.state.projectId);
|
||||||
this.props.getRemixes(this.state.projectId);
|
this.props.getRemixes(this.state.projectId);
|
||||||
this.props.getStudios(this.state.projectId);
|
this.props.getProjectStudios(this.state.projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
|
if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
|
||||||
this.getExtensions(this.state.projectId);
|
this.getExtensions(this.state.projectId);
|
||||||
|
@ -142,6 +150,13 @@ class Preview extends React.Component {
|
||||||
handleReportClose () {
|
handleReportClose () {
|
||||||
this.setState({report: {...this.state.report, open: false}});
|
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) {
|
handleReportSubmit (formData) {
|
||||||
this.setState({report: {
|
this.setState({report: {
|
||||||
category: formData.report_category,
|
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 () {
|
handleFavoriteToggle () {
|
||||||
this.props.setFavedStatus(
|
this.props.setFavedStatus(
|
||||||
!this.props.faved,
|
!this.props.faved,
|
||||||
|
@ -231,7 +262,7 @@ class Preview extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handlePermissions () {
|
handlePermissions () {
|
||||||
// TODO: handle admins and mods
|
// TODO: handle admins and mods
|
||||||
if (this.props.projectInfo.author.username === this.props.user.username) {
|
if (this.props.projectInfo.author.username === this.props.user.username) {
|
||||||
this.setState({editable: true});
|
this.setState({editable: true});
|
||||||
}
|
}
|
||||||
|
@ -280,6 +311,7 @@ class Preview extends React.Component {
|
||||||
this.props.playerMode ?
|
this.props.playerMode ?
|
||||||
<Page>
|
<Page>
|
||||||
<PreviewPresentation
|
<PreviewPresentation
|
||||||
|
addToStudioOpen={this.state.addToStudioOpen}
|
||||||
comments={this.props.comments}
|
comments={this.props.comments}
|
||||||
editable={this.state.editable}
|
editable={this.state.editable}
|
||||||
extensions={this.state.extensions}
|
extensions={this.state.extensions}
|
||||||
|
@ -294,17 +326,21 @@ class Preview extends React.Component {
|
||||||
parentInfo={this.props.parent}
|
parentInfo={this.props.parent}
|
||||||
projectId={this.state.projectId}
|
projectId={this.state.projectId}
|
||||||
projectInfo={this.props.projectInfo}
|
projectInfo={this.props.projectInfo}
|
||||||
|
projectStudios={this.props.projectStudios}
|
||||||
remixes={this.props.remixes}
|
remixes={this.props.remixes}
|
||||||
report={this.state.report}
|
report={this.state.report}
|
||||||
studios={this.props.studios}
|
studios={this.props.studios}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
userOwnsProject={this.userOwnsProject()}
|
userOwnsProject={this.userOwnsProject()}
|
||||||
|
onAddToStudioClicked={this.handleAddToStudioClick}
|
||||||
|
onAddToStudioClosed={this.handleAddToStudioClose}
|
||||||
onFavoriteClicked={this.handleFavoriteToggle}
|
onFavoriteClicked={this.handleFavoriteToggle}
|
||||||
onLoveClicked={this.handleLoveToggle}
|
onLoveClicked={this.handleLoveToggle}
|
||||||
onReportClicked={this.handleReportClick}
|
onReportClicked={this.handleReportClick}
|
||||||
onReportClose={this.handleReportClose}
|
onReportClose={this.handleReportClose}
|
||||||
onReportSubmit={this.handleReportSubmit}
|
onReportSubmit={this.handleReportSubmit}
|
||||||
onSeeInside={this.handleSeeInside}
|
onSeeInside={this.handleSeeInside}
|
||||||
|
onToggleStudio={this.handleToggleStudio}
|
||||||
onUpdate={this.handleUpdate}
|
onUpdate={this.handleUpdate}
|
||||||
/>
|
/>
|
||||||
</Page> :
|
</Page> :
|
||||||
|
@ -322,18 +358,20 @@ Preview.propTypes = {
|
||||||
comments: PropTypes.arrayOf(PropTypes.object),
|
comments: PropTypes.arrayOf(PropTypes.object),
|
||||||
faved: PropTypes.bool,
|
faved: PropTypes.bool,
|
||||||
fullScreen: PropTypes.bool,
|
fullScreen: PropTypes.bool,
|
||||||
|
getCuratedStudios: PropTypes.func.isRequired,
|
||||||
getFavedStatus: PropTypes.func.isRequired,
|
getFavedStatus: PropTypes.func.isRequired,
|
||||||
getLovedStatus: PropTypes.func.isRequired,
|
getLovedStatus: PropTypes.func.isRequired,
|
||||||
getOriginalInfo: PropTypes.func.isRequired,
|
getOriginalInfo: PropTypes.func.isRequired,
|
||||||
getParentInfo: PropTypes.func.isRequired,
|
getParentInfo: PropTypes.func.isRequired,
|
||||||
getProjectInfo: PropTypes.func.isRequired,
|
getProjectInfo: PropTypes.func.isRequired,
|
||||||
|
getProjectStudios: PropTypes.func.isRequired,
|
||||||
getRemixes: PropTypes.func.isRequired,
|
getRemixes: PropTypes.func.isRequired,
|
||||||
getStudios: PropTypes.func.isRequired,
|
|
||||||
loved: PropTypes.bool,
|
loved: PropTypes.bool,
|
||||||
original: projectShape,
|
original: projectShape,
|
||||||
parent: projectShape,
|
parent: projectShape,
|
||||||
playerMode: PropTypes.bool,
|
playerMode: PropTypes.bool,
|
||||||
projectInfo: projectShape,
|
projectInfo: projectShape,
|
||||||
|
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||||
remixes: PropTypes.arrayOf(PropTypes.object),
|
remixes: PropTypes.arrayOf(PropTypes.object),
|
||||||
sessionStatus: PropTypes.string,
|
sessionStatus: PropTypes.string,
|
||||||
setFavedStatus: PropTypes.func.isRequired,
|
setFavedStatus: PropTypes.func.isRequired,
|
||||||
|
@ -341,6 +379,7 @@ Preview.propTypes = {
|
||||||
setLovedStatus: PropTypes.func.isRequired,
|
setLovedStatus: PropTypes.func.isRequired,
|
||||||
setPlayer: PropTypes.func.isRequired,
|
setPlayer: PropTypes.func.isRequired,
|
||||||
studios: PropTypes.arrayOf(PropTypes.object),
|
studios: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
toggleStudio: PropTypes.func.isRequired,
|
||||||
updateProject: PropTypes.func.isRequired,
|
updateProject: PropTypes.func.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
|
@ -359,6 +398,40 @@ Preview.defaultProps = {
|
||||||
user: {}
|
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 => ({
|
const mapStateToProps = state => ({
|
||||||
projectInfo: state.preview.projectInfo,
|
projectInfo: state.preview.projectInfo,
|
||||||
comments: state.preview.comments,
|
comments: state.preview.comments,
|
||||||
|
@ -368,7 +441,10 @@ const mapStateToProps = state => ({
|
||||||
parent: state.preview.parent,
|
parent: state.preview.parent,
|
||||||
remixes: state.preview.remixes,
|
remixes: state.preview.remixes,
|
||||||
sessionStatus: state.session.status,
|
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,
|
user: state.session.session.user,
|
||||||
playerMode: state.scratchGui.mode.isPlayerOnly,
|
playerMode: state.scratchGui.mode.isPlayerOnly,
|
||||||
fullScreen: state.scratchGui.mode.isFullScreen
|
fullScreen: state.scratchGui.mode.isFullScreen
|
||||||
|
@ -388,8 +464,18 @@ const mapDispatchToProps = dispatch => ({
|
||||||
getRemixes: id => {
|
getRemixes: id => {
|
||||||
dispatch(previewActions.getRemixes(id));
|
dispatch(previewActions.getRemixes(id));
|
||||||
},
|
},
|
||||||
getStudios: id => {
|
getProjectStudios: id => {
|
||||||
dispatch(previewActions.getStudios(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) => {
|
getFavedStatus: (id, username, token) => {
|
||||||
dispatch(previewActions.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"?>
|
<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>
|
||||||
<!-- 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>
|
|
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="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>
|
||||||
<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>
|
|
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="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>
|
||||||
<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>
|
|
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 |