Merge pull request #2044 from LLK/release/august-2018
[Master] Release for August 2018
|
@ -5,3 +5,4 @@ intl/*
|
|||
locales/*
|
||||
**/*.min.js
|
||||
**/node_modules/*
|
||||
scratch-gui/*
|
||||
|
|
20
.travis.yml
|
@ -18,11 +18,26 @@ env:
|
|||
- API_HOST_VAR=API_HOST_$TRAVIS_BRANCH
|
||||
- API_HOST=${!API_HOST_VAR}
|
||||
- API_HOST=${API_HOST:-$API_HOST_STAGING}
|
||||
- ASSET_HOST_master=https://assets.scratch.mit.edu
|
||||
- ASSET_HOST_STAGING=https://assets.scratch.ly
|
||||
- ASSET_HOST_VAR=ASSET_HOST_$TRAVIS_BRANCH
|
||||
- ASSET_HOST=${!ASSET_HOST_VAR}
|
||||
- ASSET_HOST=${ASSET_HOST:-$ASSET_HOST_STAGING}
|
||||
- BACKPACK_HOST_master=https://backpack.scratch.mit.edu
|
||||
- BACKPACK_HOST_STAGING=https://backpack.scratch.ly
|
||||
- BACKPACK_HOST_VAR=BACKPACK_HOST_$TRAVIS_BRANCH
|
||||
- BACKPACK_HOST=${!BACKPACK_HOST_VAR}
|
||||
- BACKPACK_HOST=${BACKPACK_HOST:-$BACKPACK_HOST_STAGING}
|
||||
- ROOT_URL_master=https://scratch.mit.edu
|
||||
- ROOT_URL_STAGING=https://scratch.ly
|
||||
- ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH
|
||||
- ROOT_URL=${!ROOT_URL_VAR}
|
||||
- ROOT_URL=${ROOT_URL:-$ROOT_URL_STAGING}
|
||||
- PROJECT_HOST_master=https://projects.scratch.mit.edu
|
||||
- PROJECT_HOST_STAGING=https://projects.scratch.ly
|
||||
- PROJECT_HOST_VAR=PROJECT_HOST_$TRAVIS_BRANCH
|
||||
- PROJECT_HOST=${!PROJECT_HOST_VAR}
|
||||
- PROJECT_HOST=${PROJECT_HOST:-$PROJECT_HOST_STAGING}
|
||||
- PATH=$PATH:$PWD/test/integration/node_modules/chromedriver/bin
|
||||
- AWS_ACCESS_KEY_ID=$EB_AWS_ACCESS_KEY_ID
|
||||
- AWS_SECRET_ACCESS_KEY=$EB_AWS_SECRET_ACCESS_KEY
|
||||
|
@ -69,6 +84,7 @@ addons:
|
|||
install:
|
||||
- sudo -H pip install -r requirements.txt
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
|
@ -94,8 +110,8 @@ jobs:
|
|||
- cd test/integration
|
||||
- npm install
|
||||
- cd -
|
||||
script: npm run smoke
|
||||
script: npm run smoke-sauce
|
||||
stages:
|
||||
- test
|
||||
- name: smoke
|
||||
if: branch IN (travis) and type != pull_request
|
||||
if: type != pull_request
|
||||
|
|
2
Makefile
|
@ -70,7 +70,7 @@ integration:
|
|||
|
||||
smoke:
|
||||
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600
|
||||
|
||||
|
||||
smoke-verbose:
|
||||
$(TAP) ./test/integration/smoke-testing/*.js --timeout=3600 -R spec
|
||||
|
||||
|
|
19
README.md
|
@ -81,14 +81,17 @@ To stop the process that is making the site available to your web browser (creat
|
|||
|
||||
`npm start` can be configured with the following environment variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------- | ----------------------------- | ---------------------------------------------- |
|
||||
| `API_HOST` | `https://api.scratch.mit.edu` | Hostname for API requests |
|
||||
| `SENTRY_DSN` | `''` | DSN for Sentry |
|
||||
| `FALLBACK` | `''` | Pass-through location for old site |
|
||||
| `GA_TRACKER` | `''` | Where to log Google Analytics data |
|
||||
| `NODE_ENV` | `null` | If not `production`, app acts like development |
|
||||
| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) |
|
||||
| Variable | Default | Description |
|
||||
| --------------- | ---------------------------------- | ---------------------------------------------- |
|
||||
| `API_HOST` | `https://api.scratch.mit.edu` | Hostname for API requests |
|
||||
| `ASSETS_HOST` | `https://assets.scratch.mit.edu` | Hostname for asset requests |
|
||||
| `BACKPACK_HOST` | `https://backpack.scratch.mit.edu` | Hostname for backpack requests |
|
||||
| `PROJECTS_HOST` | `https://projects.scratch.mit.edu` | Hostname for project requests |
|
||||
| `SENTRY_DSN` | `''` | DSN for Sentry |
|
||||
| `FALLBACK` | `''` | Pass-through location for old site |
|
||||
| `GA_TRACKER` | `''` | Where to log Google Analytics data |
|
||||
| `NODE_ENV` | `null` | If not `production`, app acts like development |
|
||||
| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) |
|
||||
|
||||
**NOTE:** Because by default `API_HOST=https://api.scratch.mit.edu`, please be aware that, by default, you will be seeing and interacting with real data on the Scratch website.
|
||||
|
||||
|
|
|
@ -21,7 +21,18 @@ routes.forEach(route => {
|
|||
app.get(route.pattern, handler(route));
|
||||
});
|
||||
|
||||
app.use(webpackDevMiddleware(compiler));
|
||||
var middlewareOptions = {};
|
||||
if (process.env.USE_DOCKER_WATCHOPTIONS) {
|
||||
middlewareOptions = {
|
||||
watchOptions: {
|
||||
aggregateTimeout: 500,
|
||||
poll: 2500,
|
||||
ignored: ['node_modules', 'build']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
app.use(webpackDevMiddleware(compiler, middlewareOptions));
|
||||
|
||||
var proxyHost = process.env.FALLBACK || '';
|
||||
if (proxyHost !== '') {
|
||||
|
|
|
@ -2,7 +2,6 @@ version: '3.4'
|
|||
volumes:
|
||||
npm_data:
|
||||
runtime_data:
|
||||
intl_data:
|
||||
|
||||
networks:
|
||||
scratch-api_scratch_network:
|
||||
|
@ -15,7 +14,8 @@ services:
|
|||
environment:
|
||||
- API_HOST=http://localhost:8491
|
||||
- FALLBACK=http://localhost:8080
|
||||
build:
|
||||
- USE_DOCKER_WATCHOPTIONS=true
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
image: scratch-www:latest
|
||||
|
@ -33,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",
|
||||
|
|
|
@ -5,6 +5,7 @@ $ui-blue-10percent: hsla(215, 100, 65, .1);
|
|||
$ui-blue-25percent: hsla(215, 100, 65, .25);
|
||||
|
||||
$ui-orange: hsla(38, 100, 55, 1); // #FFAB19 Control Primary
|
||||
$ui-orange-10percent: hsla(35, 90, 55, .1);
|
||||
$ui-orange-25percent: hsla(35, 90, 55, .25);
|
||||
|
||||
$ui-light-gray: hsla(0, 0, 98, 1); //#FAFAFA
|
||||
|
@ -13,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 */
|
||||
|
@ -27,20 +27,25 @@ $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
|
||||
$type-gray: hsla(225, 15, 40, 1); //#575E75
|
||||
$type-gray-75percent: hsla(225, 15, 40, .75);
|
||||
$type-white: hsla(0, 100, 100, 1); //#FFF
|
||||
|
||||
$link-blue: $ui-blue;
|
||||
|
|
|
@ -108,7 +108,7 @@ const Footer = props => (
|
|||
</a>
|
||||
</dd>
|
||||
<dd>
|
||||
<a href="https://wiki.scratch.mit.edu/">
|
||||
<a href="https://en.scratch-wiki.info/">
|
||||
<FormattedMessage id="general.wiki" />
|
||||
</a>
|
||||
</dd>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.char-count {
|
||||
letter-spacing: 1px;
|
||||
color: lighten($type-gray, 30%);
|
||||
color: $type-gray-75percent;
|
||||
font-weight: 500;
|
||||
|
||||
&.overmax {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
@import "../../frameless";
|
||||
|
||||
$base-bg: $ui-light-gray;
|
||||
$pass-bg: lighten($ui-aqua, 35%);
|
||||
|
||||
.row {
|
||||
label {
|
||||
|
@ -32,8 +31,7 @@ $pass-bg: lighten($ui-aqua, 35%);
|
|||
}
|
||||
|
||||
&.pass {
|
||||
border: 1px solid $active-dark-gray;
|
||||
background-color: $pass-bg;
|
||||
border: 1px solid $ui-aqua;
|
||||
}
|
||||
|
||||
/* IE10/11-specific style resets */
|
||||
|
|
|
@ -28,8 +28,8 @@ module.exports.validationHOCFactory = defaultValidationErrors => (Component => {
|
|||
<Component
|
||||
validationErrors={defaults(
|
||||
{},
|
||||
defaultValidationErrors,
|
||||
props.validationErrors
|
||||
props.validationErrors,
|
||||
defaultValidationErrors
|
||||
)}
|
||||
{...omit(props, ['validationErrors'])}
|
||||
/>
|
||||
|
|
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;
|
189
src/components/modal/addtostudio/modal.scss
Normal file
|
@ -0,0 +1,189 @@
|
|||
@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%;
|
||||
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,52 @@ $modal-close-size: 1rem;
|
|||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Close button, Submit button, etc. */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
margin: 1.125rem .8275rem .9375rem .8275rem;
|
||||
line-height: 1.5rem;
|
||||
justify-content: flex-end !important;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
/* setting overall modal to contain overflow looks good, but isn't
|
||||
compatible with elements (like validation popups) that need to bleed
|
||||
past modal boundary. This class can be used to force modal button
|
||||
row to appear to contain overflow. */
|
||||
.action-buttons-overflow-fix {
|
||||
margin-bottom: .9375rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.action-button.disabled {
|
||||
background-color: $active-dark-gray;
|
||||
}
|
||||
|
||||
.error-text
|
||||
{
|
||||
display: block;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $ui-orange;
|
||||
padding: 1rem;
|
||||
min-height: 1rem;
|
||||
overflow: visible;
|
||||
color: $type-white;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
const bindAll = require('lodash.bindall');
|
||||
const PropTypes = require('prop-types');
|
||||
const React = require('react');
|
||||
const connect = require('react-redux').connect;
|
||||
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 classNames = require('classnames');
|
||||
|
||||
const Form = require('../../forms/form.jsx');
|
||||
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');
|
||||
const previewActions = require('../../../redux/preview.js');
|
||||
|
||||
require('../../forms/button.scss');
|
||||
require('./modal.scss');
|
||||
|
@ -67,12 +71,24 @@ class ReportModal extends React.Component {
|
|||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleReportCategorySelect'
|
||||
'handleCategorySelect',
|
||||
'handleValid',
|
||||
'handleInvalid'
|
||||
]);
|
||||
this.state = {reportCategory: this.props.report.category};
|
||||
this.state = {
|
||||
category: '',
|
||||
notes: '',
|
||||
valid: false
|
||||
};
|
||||
}
|
||||
handleReportCategorySelect (name, value) {
|
||||
this.setState({reportCategory: value});
|
||||
handleCategorySelect (name, value) {
|
||||
this.setState({category: value});
|
||||
}
|
||||
handleValid () {
|
||||
this.setState({valid: true});
|
||||
}
|
||||
handleInvalid () {
|
||||
this.setState({valid: false});
|
||||
}
|
||||
lookupPrompt (value) {
|
||||
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
|
||||
|
@ -81,17 +97,24 @@ class ReportModal extends React.Component {
|
|||
render () {
|
||||
const {
|
||||
intl,
|
||||
isConfirmed,
|
||||
isError,
|
||||
isOpen,
|
||||
isWaiting,
|
||||
onReport, // eslint-disable-line no-unused-vars
|
||||
report,
|
||||
onRequestClose,
|
||||
type,
|
||||
...modalProps
|
||||
} = this.props;
|
||||
const submitEnabled = this.state.valid && !isWaiting;
|
||||
const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
|
||||
const contentLabel = intl.formatMessage({id: `report.${type}`});
|
||||
return (
|
||||
<Modal
|
||||
className="mod-report"
|
||||
contentLabel={contentLabel}
|
||||
isOpen={report.open}
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
{...modalProps}
|
||||
>
|
||||
<div>
|
||||
|
@ -101,72 +124,120 @@ class ReportModal extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="report-modal-content">
|
||||
<FormattedMessage
|
||||
id={`report.${type}Instructions`}
|
||||
values={{
|
||||
CommunityGuidelinesLink: (
|
||||
<a href="/community_guidelines">
|
||||
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form
|
||||
className="report"
|
||||
onSubmit={onReport}
|
||||
>
|
||||
<Select
|
||||
required
|
||||
elementWrapperClassName="report-modal-field"
|
||||
label={null}
|
||||
name="report_category"
|
||||
options={REPORT_OPTIONS.map(option => ({
|
||||
value: option.value,
|
||||
label: this.props.intl.formatMessage(option.label)
|
||||
}))}
|
||||
value={this.state.reportCategory}
|
||||
onChange={this.handleReportCategorySelect}
|
||||
/>
|
||||
<TextArea
|
||||
required
|
||||
className="report-text"
|
||||
elementWrapperClassName="report-modal-field"
|
||||
label={null}
|
||||
name="notes"
|
||||
placeholder={this.lookupPrompt(this.state.reportCategory)}
|
||||
validationErrors={{
|
||||
maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
|
||||
minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
|
||||
}}
|
||||
validations={{
|
||||
maxLength: 500,
|
||||
minLength: 20
|
||||
}}
|
||||
value={report.notes}
|
||||
/>
|
||||
{report.waiting ? [
|
||||
<Button
|
||||
className="submit-button white"
|
||||
disabled="disabled"
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
<Spinner />
|
||||
</Button>
|
||||
] : [
|
||||
<Button
|
||||
className="submit-button white"
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage id="report.send" />
|
||||
</Button>
|
||||
]}
|
||||
</Form>
|
||||
</div>
|
||||
<Form
|
||||
className="report"
|
||||
onInvalid={this.handleInvalid}
|
||||
onValid={this.handleValid}
|
||||
onValidSubmit={onReport}
|
||||
>
|
||||
<div className="report-modal-content">
|
||||
{isConfirmed ? (
|
||||
<div className="received">
|
||||
<div className="received-header">
|
||||
<FormattedMessage id="report.receivedHeader" />
|
||||
</div>
|
||||
<FormattedMessage id="report.receivedBody" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="instructions">
|
||||
<FormattedMessage
|
||||
id={`report.${type}Instructions`}
|
||||
key={`report.${type}Instructions`}
|
||||
values={{
|
||||
CommunityGuidelinesLink: (
|
||||
<a href="/community_guidelines">
|
||||
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
required
|
||||
elementWrapperClassName="report-modal-field"
|
||||
label={null}
|
||||
name="report_category"
|
||||
options={REPORT_OPTIONS.map(option => ({
|
||||
value: option.value,
|
||||
label: this.props.intl.formatMessage(option.label),
|
||||
key: option.value
|
||||
}))}
|
||||
validationErrors={{
|
||||
isDefaultRequiredValue: this.props.intl.formatMessage({
|
||||
id: 'report.reasonMissing'
|
||||
})
|
||||
}}
|
||||
value={this.state.category}
|
||||
onChange={this.handleCategorySelect}
|
||||
/>
|
||||
<TextArea
|
||||
required
|
||||
className="report-text"
|
||||
elementWrapperClassName="report-modal-field"
|
||||
label={null}
|
||||
name="notes"
|
||||
placeholder={this.lookupPrompt(this.state.category)}
|
||||
validationErrors={{
|
||||
isDefaultRequiredValue: this.props.intl.formatMessage({
|
||||
id: 'report.textMissing'
|
||||
}),
|
||||
maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
|
||||
minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
|
||||
}}
|
||||
validations={{
|
||||
maxLength: 500,
|
||||
minLength: 20
|
||||
}}
|
||||
value={this.state.notes}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isError && (
|
||||
<div className="error-text">
|
||||
<FormattedMessage id="report.error" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FlexRow className="action-buttons">
|
||||
<div className="action-buttons-overflow-fix">
|
||||
{isConfirmed ? (
|
||||
<Button
|
||||
className="action-button submit-button"
|
||||
type="button"
|
||||
onClick={onRequestClose}
|
||||
>
|
||||
<div className="action-button-text">
|
||||
<FormattedMessage id="general.close" />
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={classNames(
|
||||
'action-button',
|
||||
'submit-button',
|
||||
{disabled: !submitEnabled}
|
||||
)}
|
||||
{...submitDisabledParam}
|
||||
key="submitButton"
|
||||
type="submit"
|
||||
>
|
||||
{isWaiting ? (
|
||||
<div className="action-button-text">
|
||||
<Spinner mode="smooth" />
|
||||
<FormattedMessage id="report.sending" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="action-button-text">
|
||||
<FormattedMessage id="report.send" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</FlexRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -174,15 +245,26 @@ class ReportModal extends React.Component {
|
|||
|
||||
ReportModal.propTypes = {
|
||||
intl: intlShape,
|
||||
isConfirmed: PropTypes.bool,
|
||||
isError: PropTypes.bool,
|
||||
isOpen: PropTypes.bool,
|
||||
isWaiting: PropTypes.bool,
|
||||
onReport: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
report: PropTypes.shape({
|
||||
category: PropTypes.string,
|
||||
notes: PropTypes.string,
|
||||
open: PropTypes.bool,
|
||||
waiting: PropTypes.bool
|
||||
}),
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = injectIntl(ReportModal);
|
||||
const mapStateToProps = state => ({
|
||||
isConfirmed: state.preview.status.report === previewActions.Status.FETCHED,
|
||||
isError: state.preview.status.report === previewActions.Status.ERROR,
|
||||
isWaiting: state.preview.status.report === previewActions.Status.FETCHING
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
const ConnectedReportModal = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ReportModal);
|
||||
|
||||
module.exports = injectIntl(ConnectedReportModal);
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
margin: 100px auto;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
width: 30rem;
|
||||
width: 36.25rem; /* 580px; */
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -34,26 +34,45 @@
|
|||
.report-modal-content {
|
||||
margin: 1rem auto;
|
||||
width: 80%;
|
||||
line-height: 1.5rem;
|
||||
font-size: .875rem;
|
||||
|
||||
|
||||
.instructions {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.received {
|
||||
margin: 0 auto;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
line-height: 1.65rem;
|
||||
|
||||
.received-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin-top: .9375rem;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
$arrow-border-width: 1rem;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(23.5rem, 0);
|
||||
left: 100%; /* position to the right of parent */
|
||||
margin-left: $arrow-border-width;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 5px;
|
||||
background-color: $ui-orange;
|
||||
padding: 1rem;
|
||||
min-width: 12rem;
|
||||
max-width: 18.75rem;
|
||||
min-height: 1rem;
|
||||
overflow: visible;
|
||||
color: $type-white;
|
||||
|
||||
/* arrow on box that points to the left */
|
||||
&:before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
@ -78,3 +97,13 @@
|
|||
.report-modal-field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group.has-error {
|
||||
.textarea, select {
|
||||
border: 1px solid $ui-orange;
|
||||
}
|
||||
}
|
||||
|
||||
.report-text .textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -249,7 +249,7 @@ class Navigation extends React.Component {
|
|||
'message-count': true,
|
||||
'show': this.props.unreadMessageCount > 0
|
||||
})}
|
||||
>{this.props.unreadMessageCount}</span>
|
||||
>{this.props.unreadMessageCount} </span>
|
||||
<FormattedMessage id="general.messages" />
|
||||
</a>
|
||||
</li>,
|
||||
|
|
|
@ -41,7 +41,7 @@ a.social-messages-profile-link {
|
|||
color: $type-gray;
|
||||
|
||||
&:hover {
|
||||
color: darken($type-gray, 10);
|
||||
color: $link-blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
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 => (
|
||||
<div
|
||||
className={`circle${id} circle`}
|
||||
key={`circle${id}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
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}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Spinner.propTypes = {
|
||||
mode: PropTypes.string
|
||||
};
|
||||
|
||||
module.exports = Spinner;
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
animation: circleFadeDelay 1.2s infinite ease-in-out both;
|
||||
margin: 0 auto;
|
||||
border-radius: 100%;
|
||||
background-color: darken($ui-white, 8%);
|
||||
background-color: $ui-gray;
|
||||
width: 15%;
|
||||
height: 15%;
|
||||
content: "";
|
||||
|
||||
.white & {
|
||||
background-color: $ui-blue-dark
|
||||
background-color: $ui-blue-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
|||
transform: rotate($rotation);
|
||||
|
||||
&:before {
|
||||
animation-delay: $delay;
|
||||
animation-delay: $delay;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,5 +54,65 @@
|
|||
|
||||
40% {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($link-blue, 40%);
|
||||
background-color: $ui-blue-10percent;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
@ -170,6 +172,7 @@
|
|||
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
|
||||
|
||||
"thumbnail.by": "by",
|
||||
"report.error": "Something went wrong when trying to send your message. Please try again.",
|
||||
"report.project": "Report Project",
|
||||
"report.projectInstructions": "From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the {CommunityGuidelinesLink}.",
|
||||
"report.CommunityGuidelinesLinkText": "Scratch Community Guidelines",
|
||||
|
@ -179,8 +182,11 @@
|
|||
"report.reasonScary": "Too Violent or Scary",
|
||||
"report.reasonLanguage": "Inappropriate Language",
|
||||
"report.reasonMusic": "Inappropriate Music",
|
||||
"report.reasonMissing": "Please select a reason",
|
||||
"report.reasonImage": "Inappropriate Images",
|
||||
"report.reasonPersonal": "Sharing Personal Contact Information",
|
||||
"report.receivedHeader": "We have received your report!",
|
||||
"report.receivedBody": "The Scratch Team will review the project based on the Scratch community guidelines.",
|
||||
"report.promptPlaceholder": "Select a reason why above.",
|
||||
"report.promptCopy": "Please provide a link to the original project",
|
||||
"report.promptUncredited": "Please provide links to the uncredited content",
|
||||
|
@ -192,5 +198,7 @@
|
|||
"report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image",
|
||||
"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.send": "Send"
|
||||
}
|
||||
"report.send": "Send",
|
||||
"report.sending": "Sending...",
|
||||
"report.textMissing": "Please tell us why you are reporting this project"
|
||||
}
|
||||
|
|
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+/})
|
||||
);
|
|
@ -74,7 +74,7 @@ p {
|
|||
padding: 1.25em;
|
||||
|
||||
&.orange {
|
||||
background-color: lighten($ui-orange, 30);
|
||||
background-color: $ui-orange-10percent;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,7 +138,7 @@ p {
|
|||
}
|
||||
|
||||
::selection {
|
||||
background-color: lighten($ui-blue, 30);
|
||||
background-color: $ui-blue-25percent;
|
||||
}
|
||||
|
||||
ol,
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const defaults = require('lodash.defaults');
|
||||
const keyMirror = require('keymirror');
|
||||
const async = require('async');
|
||||
const merge = require('lodash.merge');
|
||||
|
||||
const api = require('../lib/api');
|
||||
const log = require('../lib/log');
|
||||
|
@ -19,16 +22,22 @@ 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
|
||||
report: module.exports.Status.NOT_FETCHED,
|
||||
projectStudios: module.exports.Status.NOT_FETCHED,
|
||||
curatedStudios: module.exports.Status.NOT_FETCHED,
|
||||
studioRequests: {}
|
||||
},
|
||||
projectInfo: {},
|
||||
remixes: [],
|
||||
comments: [],
|
||||
replies: {},
|
||||
faved: false,
|
||||
loved: false,
|
||||
original: {},
|
||||
parent: {},
|
||||
studios: []
|
||||
projectStudios: [],
|
||||
curatedStudios: [],
|
||||
currentStudioIds: []
|
||||
});
|
||||
|
||||
module.exports.previewReducer = (state, action) => {
|
||||
|
@ -53,13 +62,33 @@ 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, {
|
||||
comments: action.items
|
||||
comments: [...state.comments, ...action.items] // TODO: consider a different way of doing this?
|
||||
});
|
||||
case 'SET_REPLIES':
|
||||
return Object.assign({}, state, {
|
||||
replies: merge({}, state.replies, action.replies)
|
||||
});
|
||||
case 'SET_LOVED':
|
||||
return Object.assign({}, state, {
|
||||
|
@ -73,6 +102,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 +149,48 @@ 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.setComments = items => ({
|
||||
type: 'SET_COMMENTS',
|
||||
items: items
|
||||
});
|
||||
|
||||
module.exports.setReplies = replies => ({
|
||||
type: 'SET_REPLIES',
|
||||
replies: replies
|
||||
});
|
||||
|
||||
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}`
|
||||
|
@ -212,7 +276,57 @@ module.exports.getFavedStatus = (id, username, token) => (dispatch => {
|
|||
});
|
||||
});
|
||||
|
||||
module.exports.getTopLevelComments = (id, offset) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/comments/project/${id}`,
|
||||
params: {offset: offset || 0}
|
||||
}, (err, body) => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError('No comment info'));
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setFetchStatus('comments', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setComments(body));
|
||||
dispatch(module.exports.getReplies(id, body.map(comment => comment.id)));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.getReplies = (projectId, commentIds) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHING));
|
||||
const fetchedReplies = {};
|
||||
async.eachLimit(commentIds, 10, (parentId, callback) => {
|
||||
api({
|
||||
uri: `/comments/project/${projectId}/${parentId}`
|
||||
}, (err, body) => {
|
||||
if (err) {
|
||||
return callback(`Error fetching comment replies: ${err}`);
|
||||
}
|
||||
if (typeof body === 'undefined') {
|
||||
return callback('No comment reply information');
|
||||
}
|
||||
fetchedReplies[parentId] = body;
|
||||
callback(null, body);
|
||||
});
|
||||
}, err => {
|
||||
if (err) {
|
||||
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.ERROR));
|
||||
dispatch(module.exports.setError(err));
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setFetchStatus('replies', module.exports.Status.FETCHED));
|
||||
dispatch(module.exports.setReplies(fetchedReplies));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
|
||||
if (faved) {
|
||||
api({
|
||||
uri: `/projects/${id}/favorites/user/${username}`,
|
||||
|
@ -272,6 +386,7 @@ module.exports.getLovedStatus = (id, username, token) => (dispatch => {
|
|||
});
|
||||
|
||||
module.exports.setLovedStatus = (loved, id, username, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHING));
|
||||
if (loved) {
|
||||
api({
|
||||
uri: `/projects/${id}/loves/user/${username}`,
|
||||
|
@ -333,30 +448,94 @@ 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));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
|
||||
api({
|
||||
uri: `/projects/${id}`,
|
||||
authentication: token,
|
||||
|
@ -382,3 +561,27 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
|
|||
dispatch(module.exports.setProjectInfo(body));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports.reportProject = (id, jsonData) => (dispatch => {
|
||||
dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHING));
|
||||
// scratchr2 will fail if no thumbnail base64 string provided. We don't yet have
|
||||
// a way to get the actual project thumbnail in www/gui, so for now just submit
|
||||
// a minimal base64 png string.
|
||||
defaults(jsonData, {
|
||||
thumbnail: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC' +
|
||||
'0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII='
|
||||
});
|
||||
api({
|
||||
host: '',
|
||||
uri: `/site-api/projects/all/${id}/report/`,
|
||||
method: 'POST',
|
||||
json: jsonData,
|
||||
useCsrf: true
|
||||
}, (err, body, res) => {
|
||||
if (err || res.statusCode !== 200) {
|
||||
dispatch(module.exports.setFetchStatus('report', module.exports.Status.ERROR));
|
||||
return;
|
||||
}
|
||||
dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHED));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
display: block;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($ui-blue, 40);
|
||||
background-color: $ui-blue-25percent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
}
|
||||
|
||||
.conf2017-title-band {
|
||||
background-color: lighten($ui-blue, 10%);
|
||||
background-color: $ui-blue-dark;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
color: $type-white;
|
||||
|
|
|
@ -174,6 +174,14 @@ const Credits = () => (
|
|||
<span className="name">Tracy Tang</span>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<img
|
||||
alt="Bryce Avatar"
|
||||
src="//cdn.scratch.mit.edu/get_image/user/2029640_170x170.png"
|
||||
/>
|
||||
<span className="name">Bryce Taylor</span>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<img
|
||||
alt="Matthew Avatar"
|
||||
|
@ -198,6 +206,14 @@ const Credits = () => (
|
|||
<span className="name">Chris Willis-Ford</span>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<img
|
||||
alt="Kathy Avatar"
|
||||
src="//cdn.scratch.mit.edu/get_image/user/26779669_170x170.png"
|
||||
/>
|
||||
<span className="name">Kathy Wu</span>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<img
|
||||
alt="Julia Avatar"
|
||||
|
|
|
@ -54,9 +54,9 @@
|
|||
}
|
||||
|
||||
.admin-message {
|
||||
border: 1px solid darken($ui-gray, 10);
|
||||
border: 1px solid $active-dark-gray;
|
||||
border-radius: 5px;
|
||||
background-color: lighten($ui-blue, 40);
|
||||
background-color: $ui-blue-25percent;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
|
69
src/views/preview/comment/comment.jsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
|
||||
const Avatar = require('../../../components/avatar/avatar.jsx');
|
||||
const FormattedRelative = require('react-intl').FormattedRelative;
|
||||
|
||||
require('./comment.scss');
|
||||
|
||||
const Comment = ({
|
||||
author,
|
||||
content,
|
||||
datetimeCreated,
|
||||
id
|
||||
}) => (
|
||||
<div
|
||||
className="flex-row comment"
|
||||
id={`comments-${id}`}
|
||||
>
|
||||
<a href={`/users/${author.username}`}>
|
||||
<Avatar src={author.image} />
|
||||
</a>
|
||||
<FlexRow className="comment-body column">
|
||||
<FlexRow className="comment-top-row">
|
||||
<a
|
||||
className="username"
|
||||
href={`/users/${author.username}`}
|
||||
>{author.username}</a>
|
||||
<div className="action-list">
|
||||
{/* TODO: Hook these up to API calls/logic */}
|
||||
<span className="comment-delete">Delete</span>
|
||||
<span className="comment-report">Report</span>
|
||||
</div>
|
||||
</FlexRow>
|
||||
<div className="comment-bubble">
|
||||
{/* TODO: at the moment, comment content does not properly display
|
||||
* emojis/easter eggs
|
||||
* @user links in replies
|
||||
* links to scratch.mit.edu pages
|
||||
*/}
|
||||
<span className="comment-content">{content}</span>
|
||||
<FlexRow className="comment-bottom-row">
|
||||
<span className="comment-time">
|
||||
<FormattedRelative value={new Date(datetimeCreated)} />
|
||||
</span>
|
||||
<a
|
||||
className="comment-reply"
|
||||
href={`#comments-${id}`}
|
||||
>
|
||||
reply
|
||||
</a>
|
||||
</FlexRow>
|
||||
</div>
|
||||
</FlexRow>
|
||||
</div>
|
||||
);
|
||||
|
||||
Comment.propTypes = {
|
||||
author: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
image: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
}),
|
||||
content: PropTypes.string,
|
||||
datetimeCreated: PropTypes.string,
|
||||
id: PropTypes.number
|
||||
};
|
||||
|
||||
module.exports = Comment;
|
214
src/views/preview/comment/comment.scss
Normal file
|
@ -0,0 +1,214 @@
|
|||
@import "../../../colors";
|
||||
|
||||
.compose-comment {
|
||||
width: 100%;
|
||||
|
||||
.textarea-row {
|
||||
width: 100%;
|
||||
|
||||
textarea {
|
||||
&:not(:focus) {
|
||||
border: 1px solid $active-dark-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compose-bottom-row {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
.compose-post {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.compose-cancel {
|
||||
background-color: $ui-dark-gray;
|
||||
}
|
||||
|
||||
.compose-limit {
|
||||
margin-left: auto;
|
||||
height: 100%;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 0;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.comment {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.comment-top-row {
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
|
||||
.username {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.comment-delete,
|
||||
.comment-report {
|
||||
opacity: .5;
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
|
||||
&:before {
|
||||
display: inline-block;
|
||||
margin-right: .5rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.comment-delete {
|
||||
margin-right: 1rem;
|
||||
|
||||
&:before {
|
||||
background-image: url("/svgs/project/delete-gray.svg");
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
vertical-align: -.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-report {
|
||||
&:before {
|
||||
margin-right: .25rem;
|
||||
background-image: url("/svgs/project/report-gray.svg");
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
vertical-align: -.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
margin-bottom: 1.5rem;
|
||||
min-width: 50%;
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
|
||||
.comment-bubble {
|
||||
position: relative;
|
||||
margin-left: .5rem;
|
||||
border: 1px solid $active-gray;
|
||||
border-radius: 0 .5rem .5rem .5rem;
|
||||
background-color: $ui-white;
|
||||
padding: .75rem;
|
||||
width: calc(100% - .5rem);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:before {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -11px; // width + 1px
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-radius: 0 0 0 8px;
|
||||
border-color: $active-gray transparent $active-gray $active-gray;
|
||||
background: $ui-white;
|
||||
width: 10px;
|
||||
height: 9px;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.comment-bottom-row {
|
||||
padding-top: 1rem;
|
||||
font-size: .75rem;
|
||||
justify-content: space-between;
|
||||
|
||||
.comment-time {
|
||||
color: $ui-dark-gray;
|
||||
}
|
||||
|
||||
.comment-reply {
|
||||
display: inline-flex;
|
||||
|
||||
&:after {
|
||||
margin-left: .25rem;
|
||||
background-image: url("/svgs/project/comment-reply.svg");
|
||||
background-size: cover;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.replies {
|
||||
width: calc(100% - 4rem);
|
||||
|
||||
&.collapsed .comment {
|
||||
&:last-child {
|
||||
&:after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
transparent,
|
||||
$ui-light-primary
|
||||
);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expand-thread {
|
||||
margin-bottom: 24px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-color: $active-gray;
|
||||
width: 50%;
|
||||
height: 2px;
|
||||
vertical-align: middle;
|
||||
content: "";
|
||||
}
|
||||
|
||||
&:before {
|
||||
right: .5em;
|
||||
margin-left: -50%;
|
||||
}
|
||||
|
||||
&:after {
|
||||
left: .5em;
|
||||
margin-right: -50%;
|
||||
}
|
||||
}
|
||||
|
26
src/views/preview/comment/compose-comment.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
const React = require('react');
|
||||
|
||||
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
|
||||
const InplaceInput = require('../../../components/forms/inplace-input.jsx');
|
||||
const Button = require('../../../components/forms/button.jsx');
|
||||
|
||||
require('./comment.scss');
|
||||
|
||||
const onUpdate = update => update;
|
||||
|
||||
const ComposeComment = () => (
|
||||
<FlexRow className="compose-comment column">
|
||||
<InplaceInput
|
||||
handleUpdate={onUpdate}
|
||||
name="compose-comment"
|
||||
type="textarea"
|
||||
/>
|
||||
<FlexRow className="compose-bottom-row">
|
||||
<Button className="compose-post">Post</Button>
|
||||
<Button className="compose-cancel">Cancel</Button>
|
||||
<span className="compose-limit">500 characters left</span>
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
);
|
||||
|
||||
module.exports = ComposeComment;
|
85
src/views/preview/comment/top-level-comment.jsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const bindAll = require('lodash.bindall');
|
||||
const classNames = require('classnames');
|
||||
|
||||
const FlexRow = require('../../../components/flex-row/flex-row.jsx');
|
||||
const Comment = require('./comment.jsx');
|
||||
|
||||
require('./comment.scss');
|
||||
|
||||
class TopLevelComment extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleExpandThread'
|
||||
]);
|
||||
this.state = {
|
||||
expanded: false
|
||||
};
|
||||
}
|
||||
|
||||
handleExpandThread () {
|
||||
this.setState({
|
||||
expanded: true
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
author,
|
||||
content,
|
||||
datetimeCreated,
|
||||
id,
|
||||
replies
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FlexRow className="comment-container">
|
||||
<Comment {...{author, content, datetimeCreated, id}} />
|
||||
{replies.length > 0 &&
|
||||
<FlexRow
|
||||
className={classNames(
|
||||
'replies',
|
||||
'column',
|
||||
{collapsed: !this.state.expanded && replies.length > 3}
|
||||
)}
|
||||
key={id}
|
||||
>
|
||||
{(this.state.expanded ? replies : replies.slice(0, 3)).map(reply => (
|
||||
<Comment
|
||||
author={reply.author}
|
||||
content={reply.content}
|
||||
datetimeCreated={reply.datetime_created}
|
||||
id={reply.id}
|
||||
key={reply.id}
|
||||
/>
|
||||
))}
|
||||
</FlexRow>
|
||||
}
|
||||
{!this.state.expanded && replies.length > 3 &&
|
||||
<a
|
||||
className="expand-thread"
|
||||
onClick={this.handleExpandThread}
|
||||
>See all {replies.length} replies</a>
|
||||
}
|
||||
</FlexRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TopLevelComment.propTypes = {
|
||||
author: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
image: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
}),
|
||||
content: PropTypes.string,
|
||||
datetimeCreated: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
parentId: PropTypes.number,
|
||||
projectId: PropTypes.string,
|
||||
replies: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
module.exports = TopLevelComment;
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"addToStudio.title": "Add to Studio",
|
||||
"addToStudio.finishing": "Finishing up...",
|
||||
"preview.musicExtensionChip": "Music",
|
||||
"preview.penExtensionChip": "Pen",
|
||||
"preview.speechExtensionChip": "Google Speech",
|
||||
"preview.translateExtensionChip": "Google Translate",
|
||||
"preview.videoMotionChip": "Video Motion"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,18 @@ 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 TopLevelComment = require('./comment/top-level-comment.jsx');
|
||||
const ExtensionChip = require('./extension-chip.jsx');
|
||||
|
||||
const projectShape = require('./projectshape.jsx').projectShape;
|
||||
require('./preview.scss');
|
||||
|
||||
const PreviewPresentation = ({
|
||||
assetHost,
|
||||
backpackOptions,
|
||||
comments,
|
||||
editable,
|
||||
extensions,
|
||||
faved,
|
||||
|
@ -37,17 +42,25 @@ const PreviewPresentation = ({
|
|||
loveCount,
|
||||
originalInfo,
|
||||
parentInfo,
|
||||
projectHost,
|
||||
projectId,
|
||||
projectInfo,
|
||||
remixes,
|
||||
report,
|
||||
reportOpen,
|
||||
replies,
|
||||
addToStudioOpen,
|
||||
projectStudios,
|
||||
studios,
|
||||
userOwnsProject,
|
||||
onFavoriteClicked,
|
||||
onLoadMore,
|
||||
onLoveClicked,
|
||||
onReportClicked,
|
||||
onReportClose,
|
||||
onReportSubmit,
|
||||
onAddToStudioClicked,
|
||||
onAddToStudioClosed,
|
||||
onToggleStudio,
|
||||
onSeeInside,
|
||||
onUpdate
|
||||
}) => {
|
||||
|
@ -55,21 +68,21 @@ const PreviewPresentation = ({
|
|||
return (
|
||||
<div className="preview">
|
||||
<ShareBanner shared={isShared} />
|
||||
|
||||
|
||||
{ projectInfo && projectInfo.author && projectInfo.author.id && (
|
||||
<div className="inner">
|
||||
<Formsy>
|
||||
<Formsy>
|
||||
<div className="inner">
|
||||
<FlexRow className="preview-row">
|
||||
<FlexRow className="project-header">
|
||||
<a href={`/users/${projectInfo.author.username}`}>
|
||||
<Avatar
|
||||
alt={projectInfo.author.username}
|
||||
src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo. author.id}_48x48.png`}
|
||||
src={`https://cdn2.scratch.mit.edu/get_image/user/${projectInfo.author.id}_48x48.png`}
|
||||
/>
|
||||
</a>
|
||||
<div className="title">
|
||||
{editable ?
|
||||
|
||||
|
||||
<InplaceInput
|
||||
className="project-title"
|
||||
handleUpdate={onUpdate}
|
||||
|
@ -114,10 +127,13 @@ const PreviewPresentation = ({
|
|||
<div className="guiPlayer">
|
||||
<IntlGUI
|
||||
isPlayerOnly
|
||||
assetHost={assetHost}
|
||||
backpackOptions={backpackOptions}
|
||||
basePath="/"
|
||||
className="guiPlayer"
|
||||
isFullScreen={isFullScreen}
|
||||
previewInfoVisible="false"
|
||||
projectHost={projectHost}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
|
@ -237,9 +253,24 @@ const PreviewPresentation = ({
|
|||
{/* eslint-enable react/jsx-sort-props */}
|
||||
</div>
|
||||
<FlexRow className="action-buttons">
|
||||
<Button className="action-button studio-button">
|
||||
Add to Studio
|
||||
</Button>
|
||||
{(isLoggedIn && userOwnsProject) &&
|
||||
<React.Fragment>
|
||||
<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">
|
||||
Copy Link
|
||||
</Button>
|
||||
|
@ -253,8 +284,8 @@ const PreviewPresentation = ({
|
|||
Report
|
||||
</Button>,
|
||||
<ReportModal
|
||||
isOpen={reportOpen}
|
||||
key="report-modal"
|
||||
report={report}
|
||||
type="project"
|
||||
onReport={onReportSubmit}
|
||||
onRequestClose={onReportClose}
|
||||
|
@ -277,23 +308,59 @@ const PreviewPresentation = ({
|
|||
))}
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
<FlexRow className="preview-row">
|
||||
<div className="comments-container">
|
||||
<div className="project-title" />
|
||||
</div>
|
||||
<FlexRow className="column">
|
||||
<RemixList remixes={remixes} />
|
||||
<StudioList studios={studios} />
|
||||
</div>
|
||||
<div className="project-lower-container">
|
||||
<div className="inner">
|
||||
<FlexRow className="preview-row">
|
||||
<div className="comments-container">
|
||||
<FlexRow className="comments-header">
|
||||
<h4>Comments</h4>
|
||||
{/* TODO: Add toggle comments component and logic*/}
|
||||
</FlexRow>
|
||||
<FlexRow className="comments-list">
|
||||
{comments.map(comment => (
|
||||
<TopLevelComment
|
||||
author={comment.author}
|
||||
content={comment.content}
|
||||
datetimeCreated={comment.datetime_created}
|
||||
id={comment.id}
|
||||
key={comment.id}
|
||||
parentId={comment.parent_id}
|
||||
projectId={projectId}
|
||||
replies={replies && replies[comment.id] ? replies[comment.id] : []}
|
||||
/>
|
||||
))}
|
||||
{comments.length < projectInfo.stats.comments &&
|
||||
<Button
|
||||
className="button load-more-button"
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
}
|
||||
</FlexRow>
|
||||
</div>
|
||||
<FlexRow className="column">
|
||||
<RemixList remixes={remixes} />
|
||||
<StudioList studios={projectStudios} />
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</FlexRow>
|
||||
</Formsy>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Formsy>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PreviewPresentation.propTypes = {
|
||||
addToStudioOpen: PropTypes.bool,
|
||||
assetHost: PropTypes.string,
|
||||
backpackOptions: PropTypes.shape({
|
||||
host: PropTypes.string,
|
||||
visible: PropTypes.bool
|
||||
}),
|
||||
comments: PropTypes.arrayOf(PropTypes.object),
|
||||
editable: PropTypes.bool,
|
||||
extensions: PropTypes.arrayOf(PropTypes.object),
|
||||
faved: PropTypes.bool,
|
||||
|
@ -303,24 +370,26 @@ PreviewPresentation.propTypes = {
|
|||
isShared: PropTypes.bool,
|
||||
loveCount: PropTypes.number,
|
||||
loved: PropTypes.bool,
|
||||
onAddToStudioClicked: PropTypes.func,
|
||||
onAddToStudioClosed: PropTypes.func,
|
||||
onFavoriteClicked: PropTypes.func,
|
||||
onLoadMore: 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,
|
||||
projectHost: PropTypes.string,
|
||||
projectId: PropTypes.string,
|
||||
projectInfo: projectShape,
|
||||
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
remixes: PropTypes.arrayOf(PropTypes.object),
|
||||
report: PropTypes.shape({
|
||||
category: PropTypes.string,
|
||||
notes: PropTypes.string,
|
||||
open: PropTypes.bool,
|
||||
waiting: PropTypes.bool
|
||||
}),
|
||||
replies: PropTypes.objectOf(PropTypes.array),
|
||||
reportOpen: PropTypes.bool,
|
||||
studios: PropTypes.arrayOf(PropTypes.object),
|
||||
userOwnsProject: PropTypes.bool
|
||||
};
|
||||
|
|
|
@ -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,13 +27,17 @@ class Preview extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'addEventListeners',
|
||||
'handleToggleStudio',
|
||||
'handleFavoriteToggle',
|
||||
'handleLoadMore',
|
||||
'handleLoveToggle',
|
||||
'handlePermissions',
|
||||
'handlePopState',
|
||||
'handleReportClick',
|
||||
'handleReportClose',
|
||||
'handleReportSubmit',
|
||||
'handleAddToStudioClick',
|
||||
'handleAddToStudioClose',
|
||||
'handleSeeInside',
|
||||
'handleUpdate',
|
||||
'initCounts',
|
||||
|
@ -49,12 +56,8 @@ class Preview extends React.Component {
|
|||
favoriteCount: 0,
|
||||
loveCount: 0,
|
||||
projectId: parts[1] === 'editor' ? 0 : parts[1],
|
||||
report: {
|
||||
category: '',
|
||||
notes: '',
|
||||
open: false,
|
||||
waiting: false
|
||||
}
|
||||
addToStudioOpen: false,
|
||||
reportOpen: false
|
||||
};
|
||||
this.getExtensions(this.state.projectId);
|
||||
this.addEventListeners();
|
||||
|
@ -66,17 +69,19 @@ class Preview extends React.Component {
|
|||
if (this.props.user) {
|
||||
const username = this.props.user.username;
|
||||
const token = this.props.user.token;
|
||||
this.props.getTopLevelComments(this.state.projectId, this.props.comments.length);
|
||||
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.getTopLevelComments(this.state.projectId, this.props.comments.length);
|
||||
this.props.getProjectInfo(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) {
|
||||
this.getExtensions(this.state.projectId);
|
||||
|
@ -107,7 +112,7 @@ class Preview extends React.Component {
|
|||
getExtensions (projectId) {
|
||||
storage
|
||||
.load(storage.AssetType.Project, projectId, storage.DataFormat.JSON)
|
||||
.then(projectAsset => {
|
||||
.then(projectAsset => { // NOTE: this is turning up null, breaking the line below.
|
||||
let input = projectAsset.data;
|
||||
if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
|
||||
!ArrayBuffer.isView(input)) { // taken from scratch-vm
|
||||
|
@ -137,31 +142,19 @@ class Preview extends React.Component {
|
|||
});
|
||||
}
|
||||
handleReportClick () {
|
||||
this.setState({report: {...this.state.report, open: true}});
|
||||
this.setState({reportOpen: true});
|
||||
}
|
||||
handleReportClose () {
|
||||
this.setState({report: {...this.state.report, open: false}});
|
||||
this.setState({reportOpen: false});
|
||||
}
|
||||
handleAddToStudioClick () {
|
||||
this.setState({addToStudioOpen: true});
|
||||
}
|
||||
handleAddToStudioClose () {
|
||||
this.setState({addToStudioOpen: false});
|
||||
}
|
||||
handleReportSubmit (formData) {
|
||||
this.setState({report: {
|
||||
category: formData.report_category,
|
||||
notes: formData.notes,
|
||||
open: this.state.report.open,
|
||||
waiting: true}
|
||||
});
|
||||
|
||||
const data = {
|
||||
...formData,
|
||||
id: this.state.projectId,
|
||||
user: this.props.user.username
|
||||
};
|
||||
console.log('submit report data', data); // eslint-disable-line no-console
|
||||
this.setState({report: {
|
||||
category: '',
|
||||
notes: '',
|
||||
open: false,
|
||||
waiting: false}
|
||||
});
|
||||
this.props.reportProject(this.state.projectId, formData);
|
||||
}
|
||||
handlePopState () {
|
||||
const path = window.location.pathname.toLowerCase();
|
||||
|
@ -196,6 +189,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,
|
||||
|
@ -213,6 +222,9 @@ class Preview extends React.Component {
|
|||
}));
|
||||
}
|
||||
}
|
||||
handleLoadMore () {
|
||||
this.props.getTopLevelComments(this.state.projectId, this.props.comments.length);
|
||||
}
|
||||
handleLoveToggle () {
|
||||
this.props.setLovedStatus(
|
||||
!this.props.loved,
|
||||
|
@ -231,7 +243,7 @@ class Preview extends React.Component {
|
|||
}
|
||||
}
|
||||
handlePermissions () {
|
||||
// TODO: handle admins and mods
|
||||
// TODO: handle admins and mods
|
||||
if (this.props.projectInfo.author.username === this.props.user.username) {
|
||||
this.setState({editable: true});
|
||||
}
|
||||
|
@ -280,6 +292,9 @@ class Preview extends React.Component {
|
|||
this.props.playerMode ?
|
||||
<Page>
|
||||
<PreviewPresentation
|
||||
addToStudioOpen={this.state.addToStudioOpen}
|
||||
assetHost={this.props.assetHost}
|
||||
backpackOptions={this.props.backpackOptions}
|
||||
comments={this.props.comments}
|
||||
editable={this.state.editable}
|
||||
extensions={this.state.extensions}
|
||||
|
@ -292,26 +307,37 @@ class Preview extends React.Component {
|
|||
loved={this.props.loved}
|
||||
originalInfo={this.props.original}
|
||||
parentInfo={this.props.parent}
|
||||
projectHost={this.props.projectHost}
|
||||
projectId={this.state.projectId}
|
||||
projectInfo={this.props.projectInfo}
|
||||
projectStudios={this.props.projectStudios}
|
||||
remixes={this.props.remixes}
|
||||
report={this.state.report}
|
||||
replies={this.props.replies}
|
||||
reportOpen={this.state.reportOpen}
|
||||
studios={this.props.studios}
|
||||
user={this.props.user}
|
||||
userOwnsProject={this.userOwnsProject()}
|
||||
onAddToStudioClicked={this.handleAddToStudioClick}
|
||||
onAddToStudioClosed={this.handleAddToStudioClose}
|
||||
onFavoriteClicked={this.handleFavoriteToggle}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onLoveClicked={this.handleLoveToggle}
|
||||
onReportClicked={this.handleReportClick}
|
||||
onReportClose={this.handleReportClose}
|
||||
onReportSubmit={this.handleReportSubmit}
|
||||
onSeeInside={this.handleSeeInside}
|
||||
onToggleStudio={this.handleToggleStudio}
|
||||
onUpdate={this.handleUpdate}
|
||||
/>
|
||||
</Page> :
|
||||
<IntlGUI
|
||||
enableCommunity
|
||||
hideIntro
|
||||
assetHost={this.props.assetHost}
|
||||
backpackOptions={this.props.backpackOptions}
|
||||
basePath="/"
|
||||
className="gui"
|
||||
projectHost={this.props.projectHost}
|
||||
projectId={this.state.projectId}
|
||||
/>
|
||||
);
|
||||
|
@ -319,28 +345,40 @@ class Preview extends React.Component {
|
|||
}
|
||||
|
||||
Preview.propTypes = {
|
||||
assetHost: PropTypes.string.isRequired,
|
||||
backpackOptions: PropTypes.shape({
|
||||
host: PropTypes.string,
|
||||
visible: PropTypes.bool
|
||||
}),
|
||||
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,
|
||||
getTopLevelComments: PropTypes.func.isRequired,
|
||||
loved: PropTypes.bool,
|
||||
original: projectShape,
|
||||
parent: projectShape,
|
||||
playerMode: PropTypes.bool,
|
||||
projectHost: PropTypes.string.isRequired,
|
||||
projectInfo: projectShape,
|
||||
projectStudios: PropTypes.arrayOf(PropTypes.object),
|
||||
remixes: PropTypes.arrayOf(PropTypes.object),
|
||||
replies: PropTypes.objectOf(PropTypes.array),
|
||||
reportProject: PropTypes.func,
|
||||
sessionStatus: PropTypes.string,
|
||||
setFavedStatus: PropTypes.func.isRequired,
|
||||
setFullScreen: PropTypes.func.isRequired,
|
||||
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,
|
||||
|
@ -355,10 +393,50 @@ Preview.propTypes = {
|
|||
};
|
||||
|
||||
Preview.defaultProps = {
|
||||
assetHost: process.env.ASSET_HOST,
|
||||
backpackOptions: {
|
||||
host: process.env.BACKPACK_HOST,
|
||||
visible: true
|
||||
},
|
||||
projectHost: process.env.PROJECT_HOST,
|
||||
sessionStatus: sessionActions.Status.NOT_FETCHED,
|
||||
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,
|
||||
|
@ -367,14 +445,17 @@ const mapStateToProps = state => ({
|
|||
original: state.preview.original,
|
||||
parent: state.preview.parent,
|
||||
remixes: state.preview.remixes,
|
||||
replies: state.preview.replies,
|
||||
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
|
||||
});
|
||||
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getOriginalInfo: id => {
|
||||
dispatch(previewActions.getOriginalInfo(id));
|
||||
|
@ -388,8 +469,21 @@ 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));
|
||||
}
|
||||
},
|
||||
getTopLevelComments: (id, offset) => {
|
||||
dispatch(previewActions.getTopLevelComments(id, offset));
|
||||
},
|
||||
getFavedStatus: (id, username, token) => {
|
||||
dispatch(previewActions.getFavedStatus(id, username, token));
|
||||
|
@ -406,6 +500,9 @@ const mapDispatchToProps = dispatch => ({
|
|||
refreshSession: () => {
|
||||
dispatch(sessionActions.refreshSession());
|
||||
},
|
||||
reportProject: (id, formData) => {
|
||||
dispatch(previewActions.reportProject(id, formData));
|
||||
},
|
||||
setOriginalInfo: info => {
|
||||
dispatch(previewActions.setOriginalInfo(info));
|
||||
},
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
@import "../../colors";
|
||||
@import "../../frameless";
|
||||
|
||||
/* stage size constants
|
||||
* this is a hack right now - stage includes padding of .5rem (8px) for alignment in gui
|
||||
* in www the player is placed with margin -.5rem to align the edge.
|
||||
* the height is calculated from the actual height on the page (404)
|
||||
*/
|
||||
$gui-width: 496px;
|
||||
/* stage size constants */
|
||||
$player-width: 482px;
|
||||
$player-height: 406px;
|
||||
$stage-width: 480px;
|
||||
$stage-height: 404px;
|
||||
|
||||
/* override view padding for share banner */
|
||||
#view {
|
||||
padding: 0 0 20px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gui {
|
||||
|
@ -29,34 +25,34 @@ $stage-height: 404px;
|
|||
.project-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 500;
|
||||
|
||||
|
||||
&.has-error {
|
||||
|
||||
|
||||
.validation-message {
|
||||
transform: translate(22rem, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.project-header {
|
||||
margin-right: 2rem;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
|
||||
.inplace-input {
|
||||
height: calc(3rem - 4px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
|
||||
|
||||
&.avatar {
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
|
||||
|
||||
&.remix {
|
||||
margin-right: .5em;
|
||||
width: 2rem;
|
||||
|
@ -64,14 +60,14 @@ $stage-height: 404px;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
margin-left: 1rem;
|
||||
text-align: left;
|
||||
font-size: .8rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
.validation-message {
|
||||
$arrow-border-width: 1rem;
|
||||
display: block;
|
||||
|
@ -108,8 +104,8 @@ $stage-height: 404px;
|
|||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
|
||||
.row {
|
||||
|
||||
&.has-error {
|
||||
.inplace-input,
|
||||
|
@ -122,21 +118,21 @@ $stage-height: 404px;
|
|||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.button {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.comments-container {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
|
||||
.remix-button,
|
||||
.see-inside-button {
|
||||
margin-top: 0;
|
||||
font-size: .875rem;
|
||||
font-weight: normal;
|
||||
|
||||
|
||||
&:before {
|
||||
display: inline-block;
|
||||
margin-right: .5rem;
|
||||
|
@ -152,19 +148,19 @@ $stage-height: 404px;
|
|||
|
||||
.remix-button {
|
||||
background-color: $ui-aqua;
|
||||
|
||||
|
||||
&:before {
|
||||
background-image: url("/svgs/project/remix-white.svg");
|
||||
}
|
||||
}
|
||||
|
||||
.see-inside-button {
|
||||
|
||||
|
||||
&:before {
|
||||
background-image: url("/svgs/project/see-inside-white.svg");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.preview-row {
|
||||
margin-top: 1rem;
|
||||
justify-content: space-between;
|
||||
|
@ -174,14 +170,13 @@ $stage-height: 404px;
|
|||
|
||||
.guiPlayer {
|
||||
display: inline-block;
|
||||
margin-left: -.5rem;
|
||||
width: $gui-width;
|
||||
width: $player-width;
|
||||
}
|
||||
|
||||
.project-notes {
|
||||
// not 1.5rem because of stage padding
|
||||
margin-left: 1rem;
|
||||
height: $stage-height;
|
||||
height: $player-height;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
flex-flow: column;
|
||||
|
@ -194,7 +189,7 @@ $stage-height: 404px;
|
|||
color: $type-gray;
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
|
||||
.subactions {
|
||||
margin-left: 1.5rem;
|
||||
justify-content: flex-end;
|
||||
|
@ -216,7 +211,7 @@ $stage-height: 404px;
|
|||
font-size: .875rem;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
|
||||
.description-block {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
@ -225,7 +220,7 @@ $stage-height: 404px;
|
|||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.project-textlabel {
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 1rem;
|
||||
|
@ -240,12 +235,12 @@ $stage-height: 404px;
|
|||
padding: .5rem;
|
||||
width: calc(100% - (1rem + 2px));
|
||||
overflow: auto;
|
||||
white-space: pre-line;
|
||||
white-space: pre-line;
|
||||
font-size: 1rem;
|
||||
// flex-grow
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.project-description.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -261,29 +256,29 @@ $stage-height: 404px;
|
|||
white-space: pre-line;
|
||||
// flex-grow
|
||||
flex: 1;
|
||||
|
||||
|
||||
&.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
&.textarea-row {
|
||||
border: 0;
|
||||
background-color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
&.has-error {
|
||||
.validation-message {
|
||||
transform: translate(26rem, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
& > .grow {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.copyleft {
|
||||
display: inline-block;
|
||||
transform: scale(-1, 1);
|
||||
|
@ -320,9 +315,9 @@ $stage-height: 404px;
|
|||
}
|
||||
|
||||
.project-loves {
|
||||
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
&:before {
|
||||
opacity: .5;
|
||||
background-image: url("/svgs/project/love-gray.svg");
|
||||
|
@ -330,7 +325,7 @@ $stage-height: 404px;
|
|||
}
|
||||
|
||||
.project-loves.loved {
|
||||
|
||||
|
||||
&:before {
|
||||
opacity: 1;
|
||||
background-image: url("/svgs/project/love-red.svg");
|
||||
|
@ -338,9 +333,9 @@ $stage-height: 404px;
|
|||
}
|
||||
|
||||
.project-favorites {
|
||||
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
&:before {
|
||||
opacity: .5;
|
||||
background-image: url("/svgs/project/fav-gray.svg");
|
||||
|
@ -348,7 +343,7 @@ $stage-height: 404px;
|
|||
}
|
||||
|
||||
.project-favorites.favorited {
|
||||
|
||||
|
||||
&:before {
|
||||
opacity: 1;
|
||||
background-image: url("/svgs/project/fav-yellow.svg");
|
||||
|
@ -356,7 +351,7 @@ $stage-height: 404px;
|
|||
}
|
||||
|
||||
.project-remixes {
|
||||
|
||||
|
||||
&:before {
|
||||
opacity: .5;
|
||||
background-image: url("/svgs/project/remix-gray.svg");
|
||||
|
@ -364,7 +359,7 @@ $stage-height: 404px;
|
|||
}
|
||||
|
||||
.project-views {
|
||||
|
||||
|
||||
&:before {
|
||||
opacity: .5;
|
||||
background-image: url("/svgs/project/views-gray.svg");
|
||||
|
@ -396,24 +391,24 @@ $stage-height: 404px;
|
|||
// border-color: transparent;
|
||||
// background-color: $active-gray;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// &:active {
|
||||
// border: 0 solid transparent;
|
||||
// box-shadow: inset 0 0 5px $box-shadow-gray;
|
||||
// background-color: $active-dark-gray;
|
||||
// padding: calc(.75em + 1px) calc(1.5em + 1px);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// &.report {
|
||||
// border: 1px solid $ui-coral;
|
||||
// background-color: $ui-coral;
|
||||
//
|
||||
//
|
||||
// &:hover {
|
||||
// transition: background-color .25s ease;
|
||||
// border-color: transparent;
|
||||
// background-color: $active-gray;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// &:active {
|
||||
// border: 0 solid transparent;
|
||||
// box-shadow: inset 0 0 5px $box-shadow-gray;
|
||||
|
@ -422,7 +417,16 @@ $stage-height: 404px;
|
|||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
.comments-header {
|
||||
padding: 0 0 1rem 0;
|
||||
justify-content: space-between;
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-button,
|
||||
.copy-link-button,
|
||||
.report-button {
|
||||
|
@ -452,29 +456,45 @@ $stage-height: 404px;
|
|||
|
||||
.report-button {
|
||||
background-color: $ui-coral;
|
||||
|
||||
|
||||
&:before {
|
||||
background-image: url("/svgs/project/report-white.svg");
|
||||
}
|
||||
}
|
||||
|
||||
.project-lower-container {
|
||||
margin-top: 1rem;
|
||||
background-color: $ui-blue-10percent;
|
||||
padding: 1rem 0;
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
.create-comment {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.load-more-button {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.extension-list {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.remix-list,
|
||||
.studio-list {
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
.project {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.creator-image img {
|
||||
max-width: 2rem;
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.thumbnail-column {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
|
4
src/views/search/l10n.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"search.trending": "Trending",
|
||||
"search.popular": "Popular"
|
||||
}
|
|
@ -8,14 +8,18 @@ const React = require('react');
|
|||
|
||||
const api = require('../../lib/api');
|
||||
const Button = require('../../components/forms/button.jsx');
|
||||
const Form = require('../../components/forms/form.jsx');
|
||||
const Grid = require('../../components/grid/grid.jsx');
|
||||
const navigationActions = require('../../redux/navigation.js');
|
||||
const Select = require('../../components/forms/select.jsx');
|
||||
const TitleBanner = require('../../components/title-banner/title-banner.jsx');
|
||||
const Tabs = require('../../components/tabs/tabs.jsx');
|
||||
|
||||
const Page = require('../../components/page/www/page.jsx');
|
||||
const render = require('../../lib/render.jsx');
|
||||
|
||||
const ACCEPTABLE_MODES = ['trending', 'popular', ''];
|
||||
|
||||
require('./search.scss');
|
||||
|
||||
class Search extends React.Component {
|
||||
|
@ -23,14 +27,31 @@ class Search extends React.Component {
|
|||
super(props);
|
||||
bindAll(this, [
|
||||
'getSearchState',
|
||||
'handleChangeSortMode',
|
||||
'handleGetSearchMore',
|
||||
'getTab'
|
||||
]);
|
||||
this.state = this.getSearchState();
|
||||
this.state.loaded = [];
|
||||
this.state.loadNumber = 16;
|
||||
this.state.mode = '';
|
||||
this.state.offset = 0;
|
||||
this.state.loadMore = false;
|
||||
|
||||
let mode = '';
|
||||
const query = window.location.search;
|
||||
const m = query.lastIndexOf('mode=');
|
||||
if (m !== -1) {
|
||||
mode = query.substring(m + 5, query.length).toLowerCase();
|
||||
}
|
||||
while (mode.indexOf('/') > -1) {
|
||||
mode = mode.substring(0, mode.indexOf('/'));
|
||||
}
|
||||
while (mode.indexOf('&') > -1) {
|
||||
mode = mode.substring(0, mode.indexOf('&'));
|
||||
}
|
||||
mode = decodeURIComponent(mode.split('+').join(' '));
|
||||
this.state.mode = mode;
|
||||
}
|
||||
componentDidMount () {
|
||||
const query = window.location.search;
|
||||
|
@ -65,6 +86,13 @@ class Search extends React.Component {
|
|||
loadNumber: 16
|
||||
};
|
||||
}
|
||||
handleChangeSortMode (name, value) {
|
||||
if (ACCEPTABLE_MODES.indexOf(value) !== -1) {
|
||||
const term = this.props.searchTerm.split(' ').join('+');
|
||||
window.location =
|
||||
`${window.location.origin}/search/${this.state.tab}?q=${term}&mode=${value}`;
|
||||
}
|
||||
}
|
||||
handleGetSearchMore () {
|
||||
let termText = '';
|
||||
if (this.props.searchTerm !== '') {
|
||||
|
@ -73,7 +101,8 @@ class Search extends React.Component {
|
|||
const locale = this.props.intl.locale;
|
||||
const loadNumber = this.state.loadNumber;
|
||||
const offset = this.state.offset;
|
||||
const queryString = `limit=${loadNumber}&offset=${offset}&language=${locale}&mode=popular${termText}`;
|
||||
const mode = this.state.mode;
|
||||
const queryString = `limit=${loadNumber}&offset=${offset}&language=${locale}&mode=${mode}${termText}`;
|
||||
|
||||
api({
|
||||
uri: `/search/${this.state.tab}?${queryString}`
|
||||
|
@ -167,6 +196,25 @@ class Search extends React.Component {
|
|||
{this.getTab('projects')}
|
||||
{this.getTab('studios')}
|
||||
</Tabs>
|
||||
<div className="sort-controls">
|
||||
<Form className="sort-mode">
|
||||
<Select
|
||||
name="sort"
|
||||
options={[
|
||||
{
|
||||
value: 'trending',
|
||||
label: this.props.intl.formatMessage({id: 'search.trending'})
|
||||
},
|
||||
{
|
||||
value: 'popular',
|
||||
label: this.props.intl.formatMessage({id: 'search.popular'})
|
||||
}
|
||||
]}
|
||||
value={this.state.mode}
|
||||
onChange={this.handleChangeSortMode}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
{this.getProjectBox()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -104,15 +104,40 @@ $base-bg: $ui-white;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* HACK: sort controls are terrible. There's some sort of magic formula for height of formsy components that I can't control. */
|
||||
|
||||
.select {
|
||||
select {
|
||||
margin-bottom: 0;
|
||||
color: $header-gray;
|
||||
}
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
border-bottom: 1px solid $ui-border;
|
||||
padding: 8px 0;
|
||||
width: 58.75rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
display: none;
|
||||
.sort-mode {
|
||||
margin-top: -4px;
|
||||
width: 13.75rem;
|
||||
|
||||
.select {
|
||||
|
||||
select {
|
||||
margin-bottom: 0;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
height: 32px;
|
||||
color: $header-gray;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.help-block {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -466,7 +466,7 @@ const Terms = () => (
|
|||
<p>
|
||||
This document, together with all appendices, constitutes the entire Terms
|
||||
of Use and supersedes all previous agreements with the Scratch Team relating
|
||||
to the use of Scratch. Revision date: 4 March 2015.
|
||||
to the use of Scratch. Revision date: April 2016.
|
||||
</p>
|
||||
</section>
|
||||
<section id="appendix-a">
|
||||
|
|
|
@ -31,25 +31,24 @@ $base-bg: $ui-white;
|
|||
max-width: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
$darken-button: rgba(0, 0, 0, .1);
|
||||
|
||||
.tips-button {
|
||||
margin-right: .75rem;
|
||||
background-color: $ui-blue;
|
||||
color: $ui-white;
|
||||
font-size: 1rem;
|
||||
|
||||
|
||||
&.getting-started-button {
|
||||
margin-right: 0;
|
||||
background-color: $darken-button;
|
||||
background-color: $ui-white;
|
||||
color: $link-blue;
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
margin-right: 1rem;
|
||||
height: 1.25rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: $ui-white;
|
||||
}
|
||||
|
@ -95,26 +94,26 @@ img.tips-icon {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ttt-head {
|
||||
|
||||
|
||||
p {
|
||||
max-width: $cols4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//put the image first if in 4-column
|
||||
.tips-info-body {
|
||||
max-width: $cols4;
|
||||
text-align: center;
|
||||
|
||||
|
||||
&.tips-illustration {
|
||||
order: -1;
|
||||
img {
|
||||
width: $cols4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -131,13 +130,13 @@ img.tips-icon {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ttt-head {
|
||||
p {
|
||||
max-width: $cols6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tips-info-body.tips-illustration {
|
||||
order: -1;
|
||||
img {
|
||||
|
@ -163,27 +162,27 @@ img.tips-icon {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ttt-head {
|
||||
p {
|
||||
max-width: $cols6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tips-info-section {
|
||||
&.mod-align-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tips-info-body {
|
||||
max-width: $cols4;
|
||||
}
|
||||
|
||||
|
||||
.tips-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
img.mod-flow-left {
|
||||
transform: translate(-1*$cols2);
|
||||
}
|
||||
|
@ -200,19 +199,19 @@ img.tips-icon {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ttt-head {
|
||||
p {
|
||||
max-width: $cols8;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tips-info-section {
|
||||
&.mod-align-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tips-info-body {
|
||||
max-width: $cols6;
|
||||
&.mod-narrow {
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="27px" height="28px" viewBox="0 0 27 28" 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>blocks 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="12-col" transform="translate(-496.000000, -285.000000)" fill="#FFFFFF">
|
||||
<g id="Band---Masthead" transform="translate(0.000000, 52.000000)">
|
||||
<g id="buttons-v1" transform="translate(474.000000, 214.000000)">
|
||||
<g id="guide-tutorial">
|
||||
<g id="blocks-icon" transform="translate(22.000000, 19.000000)">
|
||||
<path d="M0.850455083,0.114928725 L2.97546016,0.114928725 C3.19321976,0.114928725 3.40235116,0.203530037 3.55642635,0.360996823 L4.71835875,1.54850635 C4.87243394,1.70597313 5.08156534,1.79457444 5.29973581,1.79457444 L7.90545549,1.79457444 C8.12362597,1.79457444 8.33275736,1.70597313 8.48683256,1.54850635 L9.64876495,0.360996823 C9.80284014,0.203530037 10.0119715,0.114928725 10.230142,0.114928725 L25.5024861,0.114928725 C25.9560835,0.114928725 26.3242205,0.491169366 26.3242205,0.954751584 L26.3242205,5.99368874 C26.3242205,6.45727096 25.9560835,6.8335116 25.5024861,6.8335116 L10.230142,6.8335116 C10.0119715,6.8335116 9.80284014,6.92211291 9.64876495,7.0795797 L8.48683256,8.26708922 C8.33275736,8.42455601 8.12362597,8.51315732 7.90545549,8.51315732 L5.29973581,8.51315732 C5.08156534,8.51315732 4.87243394,8.42455601 4.71835875,8.26708922 L3.55642635,7.0795797 C3.40235116,6.92211291 3.19321976,6.8335116 2.97546016,6.8335116 L0.850455083,6.8335116 C0.396857712,6.8335116 0.028720716,6.45727096 0.028720716,5.99368874 L0.028720716,0.954751584 C0.028720716,0.491169366 0.396857712,0.114928725 0.850455083,0.114928725 Z" id="Stroke-1"></path>
|
||||
<path d="M0.850455083,9.8008857 L2.97546016,9.8008857 C3.19321976,9.8008857 3.40235116,9.88948701 3.55642635,10.0469538 L4.71835875,11.2344633 C4.87243394,11.3919301 5.08156534,11.4805314 5.29973581,11.4805314 L7.90545549,11.4805314 C8.12362597,11.4805314 8.33275736,11.3919301 8.48683256,11.2344633 L9.64876495,10.0469538 C9.80284014,9.88948701 10.0119715,9.8008857 10.230142,9.8008857 L19.7503455,9.8008857 C20.2039429,9.8008857 20.5720799,10.1771263 20.5720799,10.6407086 L20.5720799,15.6796457 C20.5720799,16.1432279 20.2039429,16.5194686 19.7503455,16.5194686 L10.230142,16.5194686 C10.0119715,16.5194686 9.80284014,16.6080699 9.64876495,16.7655367 L8.48683256,17.9530462 C8.33275736,18.110513 8.12362597,18.1991143 7.90545549,18.1991143 L5.29973581,18.1991143 C5.08156534,18.1991143 4.87243394,18.110513 4.71835875,17.9530462 L3.55642635,16.7655367 C3.40235116,16.6080699 3.19321976,16.5194686 2.97546016,16.5194686 L0.850455083,16.5194686 C0.396857712,16.5194686 0.028720716,16.1432279 0.028720716,15.6796457 L0.028720716,10.6407086 C0.028720716,10.1771263 0.396857712,9.8008857 0.850455083,9.8008857 Z" id="Stroke-3"></path>
|
||||
<path d="M0.850455083,19.4868427 L2.97546016,19.4868427 C3.19321976,19.4868427 3.40235116,19.575444 3.55642635,19.7329108 L4.71835875,20.9204203 C4.87243394,21.0778871 5.08156534,21.1664884 5.29973581,21.1664884 L7.90545549,21.1664884 C8.12362597,21.1664884 8.33275736,21.0778871 8.48683256,20.9204203 L9.64876495,19.7329108 C9.80284014,19.575444 10.0119715,19.4868427 10.230142,19.4868427 L23.037283,19.4868427 C23.4908804,19.4868427 23.8590174,19.8630833 23.8590174,20.3266655 L23.8590174,25.3656027 C23.8590174,25.8291849 23.4908804,26.2054256 23.037283,26.2054256 L10.230142,26.2054256 C10.0119715,26.2054256 9.80284014,26.2940269 9.64876495,26.4514937 L8.48683256,27.6390032 C8.33275736,27.79647 8.12362597,27.8850713 7.90545549,27.8850713 L5.29973581,27.8850713 C5.08156534,27.8850713 4.87243394,27.79647 4.71835875,27.6390032 L3.55642635,26.4514937 C3.40235116,26.2940269 3.19321976,26.2054256 2.97546016,26.2054256 L0.850455083,26.2054256 C0.396857712,26.2054256 0.028720716,25.8291849 0.028720716,25.3656027 L0.028720716,20.3266655 C0.028720716,19.8630833 0.396857712,19.4868427 0.850455083,19.4868427 Z" id="Stroke-5"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, 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="27px" height="28px" viewBox="0 0 27 28" style="enable-background:new 0 0 27 28;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#4C97FF;}
|
||||
</style>
|
||||
<title>blocks icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1">
|
||||
<g id="_x31_2-col" transform="translate(-496.000000, -285.000000)">
|
||||
<g id="Band---Masthead" transform="translate(0.000000, 52.000000)">
|
||||
<g id="buttons-v1" transform="translate(474.000000, 214.000000)">
|
||||
<g id="guide-tutorial">
|
||||
<g id="blocks-icon" transform="translate(22.000000, 19.000000)">
|
||||
<path id="Stroke-1" class="st0" d="M0.9,0.1H3c0.2,0,0.4,0.1,0.6,0.2l1.2,1.2c0.2,0.2,0.4,0.2,0.6,0.2h2.6
|
||||
c0.2,0,0.4-0.1,0.6-0.2l1.2-1.2c0.2-0.2,0.4-0.2,0.6-0.2h15.3c0.5,0,0.8,0.4,0.8,0.8v5c0,0.5-0.4,0.8-0.8,0.8H10.2
|
||||
c-0.2,0-0.4,0.1-0.6,0.2L8.5,8.3C8.3,8.4,8.1,8.5,7.9,8.5H5.3c-0.2,0-0.4-0.1-0.6-0.2L3.6,7.1C3.4,6.9,3.2,6.8,3,6.8H0.9
|
||||
C0.4,6.8,0,6.5,0,6V1C0,0.5,0.4,0.1,0.9,0.1z"/>
|
||||
<path id="Stroke-3" class="st0" d="M0.9,9.8H3c0.2,0,0.4,0.1,0.6,0.2l1.2,1.2c0.2,0.2,0.4,0.2,0.6,0.2h2.6
|
||||
c0.2,0,0.4-0.1,0.6-0.2L9.6,10c0.2-0.2,0.4-0.2,0.6-0.2h9.5c0.5,0,0.8,0.4,0.8,0.8v5c0,0.5-0.4,0.8-0.8,0.8h-9.5
|
||||
c-0.2,0-0.4,0.1-0.6,0.2L8.5,18c-0.2,0.2-0.4,0.2-0.6,0.2H5.3c-0.2,0-0.4-0.1-0.6-0.2l-1.2-1.2c-0.2-0.2-0.4-0.2-0.6-0.2H0.9
|
||||
c-0.5,0-0.8-0.4-0.8-0.8v-5C0,10.2,0.4,9.8,0.9,9.8z"/>
|
||||
<path id="Stroke-5" class="st0" d="M0.9,19.5H3c0.2,0,0.4,0.1,0.6,0.2l1.2,1.2c0.2,0.2,0.4,0.2,0.6,0.2h2.6
|
||||
c0.2,0,0.4-0.1,0.6-0.2l1.2-1.2c0.2-0.2,0.4-0.2,0.6-0.2H23c0.5,0,0.8,0.4,0.8,0.8v5c0,0.5-0.4,0.8-0.8,0.8H10.2
|
||||
c-0.2,0-0.4,0.1-0.6,0.2l-1.2,1.2c-0.2,0.2-0.4,0.2-0.6,0.2H5.3c-0.2,0-0.4-0.1-0.6-0.2l-1.2-1.2c-0.2-0.2-0.4-0.2-0.6-0.2
|
||||
H0.9c-0.5,0-0.8-0.4-0.8-0.8v-5C0,19.9,0.4,19.5,0.9,19.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.1 KiB |
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 |
10
static/svgs/project/comment-reply.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 51.1 (57501) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>comment-reply</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="comment-reply" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M9.4379644,9.95564876 C9.4397719,9.83000629 9.4365177,9.69150012 9.42535762,9.54182983 C9.39320024,9.16970313 9.30581606,8.72820912 9.12405695,8.26937328 C9.07512181,8.1566514 9.02758481,8.03887149 8.96746449,7.92470447 C8.89406178,7.78886016 8.89825622,7.75634423 8.7905989,7.59665491 C8.62352034,7.33435977 8.47531676,7.14143195 8.29006229,6.92104623 C7.92234964,6.50701011 7.47144725,6.15222524 6.99467914,5.89065268 C6.51441566,5.63486073 6.01387905,5.47228111 5.5671711,5.38846228 C5.12465758,5.30464345 4.71639867,5.29597253 4.48150998,5.29741768 C4.35847305,5.29452738 4.20677411,5.32270785 4.12917695,5.32632073 C4.04388999,5.33571422 3.99845021,5.34004968 3.99845021,5.34004968 C3.50070989,5.39279773 3.05540009,5.0185033 3.0050668,4.5040291 C2.96242332,4.07337373 3.20989533,3.68173747 3.58180242,3.53433194 C3.58180242,3.53433194 3.62514497,3.51771269 3.70553842,3.48591934 C3.79501983,3.454126 3.88310309,3.40571339 4.07395015,3.34862988 C4.46403315,3.22506936 4.96317161,3.09789597 5.6119118,3.03286412 C6.25575847,2.96999999 7.02893374,2.9880644 7.85034507,3.16076009 C8.66826104,3.33851382 9.53091772,3.67740202 10.3222689,4.17525697 C10.7053612,4.422378 11.1087266,4.72441482 11.4247078,5.00910981 C11.5596289,5.12183169 11.8001102,5.36533984 11.9385268,5.51708083 C12.1000127,5.68616364 12.2468182,5.86536252 12.3922255,6.04311624 C12.9591741,6.76424721 13.37722,7.545352 13.6547522,8.2534766 C13.9134224,8.91884719 14.055538,9.51573832 14.1351159,9.95564876 L15.4897593,9.95564876 C16.1024852,9.95564876 16.6500676,10.3333021 16.8842064,10.9178352 C17.1192892,11.5033442 16.9908905,12.1708245 16.5575451,12.6187388 L12.7603439,16.5426249 C12.4752234,16.8373311 12.0956921,17.0002978 11.6935022,17.0002978 C11.2903682,17.0002978 10.9108369,16.8373311 10.6266605,16.5426249 L6.82851524,12.6187388 C6.39611391,12.1708245 6.26677115,11.5033442 6.50185397,10.9178352 C6.73693679,10.3333021 7.28451925,9.95564876 7.89630104,9.95564876 L9.4379644,9.95564876 Z" fill="#4C97FF" transform="translate(10.000298, 10.000000) scale(-1, 1) rotate(-90.000000) translate(-10.000298, -10.000000) "></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -3,6 +3,8 @@ const bindAll = require('lodash.bindall');
|
|||
|
||||
const headless = process.env.SMOKE_HEADLESS || false;
|
||||
const remote = process.env.SMOKE_REMOTE || false;
|
||||
const ci = process.env.CI || false;
|
||||
const buildID = process.env.TRAVIS_BUILD_NUMBER;
|
||||
const {SAUCE_USERNAME, SAUCE_ACCESS_KEY} = process.env;
|
||||
const {By, until} = webdriver;
|
||||
|
||||
|
@ -24,7 +26,13 @@ class SeleniumHelper {
|
|||
}
|
||||
buildDriver (name) {
|
||||
if (remote === 'true'){
|
||||
this.driver = this.getSauceDriver(SAUCE_USERNAME, SAUCE_ACCESS_KEY, name);
|
||||
let nameToUse;
|
||||
if (ci === 'true'){
|
||||
nameToUse = 'travis ' + buildID + ' : ' + name;
|
||||
} else {
|
||||
nameToUse = name;
|
||||
}
|
||||
this.driver = this.getSauceDriver(SAUCE_USERNAME, SAUCE_ACCESS_KEY, nameToUse);
|
||||
} else {
|
||||
this.driver = this.getDriver();
|
||||
}
|
||||
|
|
|
@ -165,6 +165,9 @@ module.exports = {
|
|||
'process.env.NODE_ENV': '"' + (process.env.NODE_ENV || 'development') + '"',
|
||||
'process.env.SENTRY_DSN': '"' + (process.env.SENTRY_DSN || '') + '"',
|
||||
'process.env.API_HOST': '"' + (process.env.API_HOST || 'https://api.scratch.mit.edu') + '"',
|
||||
'process.env.ASSET_HOST': '"' + (process.env.ASSET_HOST || 'https://assets.scratch.mit.edu') + '"',
|
||||
'process.env.BACKPACK_HOST': '"' + (process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu') + '"',
|
||||
'process.env.PROJECT_HOST': '"' + (process.env.PROJECT_HOST || 'https://projects.scratch.mit.edu') + '"',
|
||||
'process.env.SCRATCH_ENV': '"' + (process.env.SCRATCH_ENV || 'development') + '"'
|
||||
}),
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
|
|