formatting readme
This commit is contained in:
commit
c95d3ac671
55 changed files with 2112 additions and 157 deletions
3
.babelrc
3
.babelrc
|
@ -6,7 +6,8 @@
|
||||||
"transform-react-remove-prop-types",
|
"transform-react-remove-prop-types",
|
||||||
"transform-react-constant-elements",
|
"transform-react-constant-elements",
|
||||||
"transform-react-inline-elements"
|
"transform-react-inline-elements"
|
||||||
]
|
],
|
||||||
|
"presets": ["es2015", "react", "react-optimize", "es2015-native-modules", "stage-0"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,5 +4,5 @@ node_modules/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
dump.rdb
|
dump.rdb
|
||||||
public/*
|
public/*
|
||||||
static/dist
|
static/dist/
|
||||||
static/css/app.min.css
|
static/css/app.min.css
|
14
README.md
14
README.md
|
@ -12,10 +12,14 @@ This project is currently in the early stages of development! It will definitely
|
||||||
MONGO_URL=mongodb://localhost:27017/p5js-web-editor
|
MONGO_URL=mongodb://localhost:27017/p5js-web-editor
|
||||||
PORT=8000
|
PORT=8000
|
||||||
SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production
|
SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production
|
||||||
|
AWS_ACCESS_KEY=<your-aws-access-key>
|
||||||
|
AWS_SECRET_KEY=<your-aws-secret-key>
|
||||||
|
S3_BUCKET=<your-s3-bucket>
|
||||||
```
|
```
|
||||||
Or, if you don't want to do that, just ask me to send you mine.
|
Or, if you don't want to do that, just ask me to send you mine. Refer to [this gist](https://gist.github.com/catarak/70c9301f0fd1ac2d6b58de03f61997e3) for creating an S3 bucket for testing, or if you don't want to do that, I will send you my AWS credentials.
|
||||||
|
|
||||||
5. `$ npm start`
|
5. `$ npm start`
|
||||||
6. Navigate to [http://localhost:8000](http://localhost:8000) in your browser
|
6. Navigate to (http://localhost:8000)[http://localhost:8000] in your browser
|
||||||
7. Install the [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en).
|
7. Install the [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en).
|
||||||
8. Open and close the Redux DevTools using `ctrl+h`, and move them with `ctrl+w`
|
8. Open and close the Redux DevTools using `ctrl+h`, and move them with `ctrl+w`
|
||||||
|
|
||||||
|
@ -28,8 +32,12 @@ This project is currently in the early stages of development! It will definitely
|
||||||
MONGO_URL=mongodb://localhost:27017/p5js-web-editor
|
MONGO_URL=mongodb://localhost:27017/p5js-web-editor
|
||||||
PORT=8000
|
PORT=8000
|
||||||
SESSION_SECRET=make_this_a_long-random_string_like_maybe_126_characters_long
|
SESSION_SECRET=make_this_a_long-random_string_like_maybe_126_characters_long
|
||||||
|
AWS_ACCESS_KEY=<your-aws-access-key>
|
||||||
|
AWS_SECRET_KEY=<your-aws-secret-key>
|
||||||
|
S3_BUCKET=<your-s3-bucket>
|
||||||
```
|
```
|
||||||
Or, if you don't want to do that, just ask me to send you mine.
|
Or, if you don't want to do that, just ask me to send you mine. Refer to [this gist](https://gist.github.com/catarak/70c9301f0fd1ac2d6b58de03f61997e3) for creating an S3 bucket for testing, or if you don't want to do that, I will send you my AWS credentials.
|
||||||
|
|
||||||
5. `$ npm run build`
|
5. `$ npm run build`
|
||||||
6. `$ npm run start:prod`
|
6. `$ npm run start:prod`
|
||||||
|
|
||||||
|
|
|
@ -3,23 +3,23 @@ import { Link } from 'react-router';
|
||||||
|
|
||||||
function Nav(props) {
|
function Nav(props) {
|
||||||
return (
|
return (
|
||||||
<nav className="nav">
|
<nav className="nav" role="navigation" title="main-navigation">
|
||||||
<ul className="nav__items-left">
|
<ul className="nav__items-left" title="project-menu">
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<p
|
<a
|
||||||
className="nav__new"
|
className="nav__new"
|
||||||
onClick={props.createProject}
|
onClick={props.createProject}
|
||||||
>
|
>
|
||||||
New
|
New
|
||||||
</p>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<p
|
<a
|
||||||
className="nav__save"
|
className="nav__save"
|
||||||
onClick={props.saveProject}
|
onClick={props.saveProject}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</p>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<p className="nav__open">
|
<p className="nav__open">
|
||||||
|
@ -28,8 +28,18 @@ function Nav(props) {
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
<li className="nav__item">
|
||||||
|
<a className="nav__export" onClick={props.exportProjectAsZip}>
|
||||||
|
Export (zip)
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="nav__item" onClick={props.cloneProject}>
|
||||||
|
<a className="nav__clone">
|
||||||
|
Clone
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="nav__items-right">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
{props.user.authenticated && <p>Hello, {props.user.username}!</p>}
|
{props.user.authenticated && <p>Hello, {props.user.username}!</p>}
|
||||||
{!props.user.authenticated && <p><Link to="/login">Login</Link> or <Link to="/signup">Sign Up</Link></p>}
|
{!props.user.authenticated && <p><Link to="/login">Login</Link> or <Link to="/signup">Sign Up</Link></p>}
|
||||||
|
@ -42,6 +52,8 @@ function Nav(props) {
|
||||||
Nav.propTypes = {
|
Nav.propTypes = {
|
||||||
createProject: PropTypes.func.isRequired,
|
createProject: PropTypes.func.isRequired,
|
||||||
saveProject: PropTypes.func.isRequired,
|
saveProject: PropTypes.func.isRequired,
|
||||||
|
exportProjectAsZip: PropTypes.func.isRequired,
|
||||||
|
cloneProject: PropTypes.func.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
authenticated: PropTypes.bool.isRequired,
|
authenticated: PropTypes.bool.isRequired,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
|
|
|
@ -8,6 +8,12 @@ export const OPEN_PREFERENCES = 'OPEN_PREFERENCES';
|
||||||
export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES';
|
export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES';
|
||||||
export const INCREASE_FONTSIZE = 'INCREASE_FONTSIZE';
|
export const INCREASE_FONTSIZE = 'INCREASE_FONTSIZE';
|
||||||
export const DECREASE_FONTSIZE = 'DECREASE_FONTSIZE';
|
export const DECREASE_FONTSIZE = 'DECREASE_FONTSIZE';
|
||||||
|
export const UPDATE_FONTSIZE = 'UPDATE_FONTSIZE';
|
||||||
|
export const INCREASE_INDENTATION = 'INCREASE_INDENTATION';
|
||||||
|
export const DECREASE_INDENTATION = 'DECREASE_INDENTATION';
|
||||||
|
export const UPDATE_INDENTATION = 'UPDATE_INDENTATION';
|
||||||
|
export const INDENT_WITH_SPACE = 'INDENT_WITH_SPACE';
|
||||||
|
export const INDENT_WITH_TAB = 'INDENT_WITH_TAB';
|
||||||
|
|
||||||
export const AUTH_USER = 'AUTH_USER';
|
export const AUTH_USER = 'AUTH_USER';
|
||||||
export const UNAUTH_USER = 'UNAUTH_USER';
|
export const UNAUTH_USER = 'UNAUTH_USER';
|
||||||
|
@ -24,6 +30,17 @@ export const SET_PROJECT = 'SET_PROJECT';
|
||||||
export const SET_PROJECTS = 'SET_PROJECTS';
|
export const SET_PROJECTS = 'SET_PROJECTS';
|
||||||
|
|
||||||
export const SET_SELECTED_FILE = 'SET_SELECTED_FILE';
|
export const SET_SELECTED_FILE = 'SET_SELECTED_FILE';
|
||||||
|
export const SHOW_MODAL = 'SHOW_MODAL';
|
||||||
|
export const HIDE_MODAL = 'HIDE_MODAL';
|
||||||
|
export const CREATE_FILE = 'CREATE_FILE';
|
||||||
|
export const SET_BLOB_URL = 'SET_BLOB_URL';
|
||||||
|
|
||||||
|
export const EXPAND_SIDEBAR = 'EXPAND_SIDEBAR';
|
||||||
|
export const COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR';
|
||||||
|
|
||||||
|
export const CONSOLE_EVENT = 'CONSOLE_EVENT';
|
||||||
|
export const EXPAND_CONSOLE = 'EXPAND_CONSOLE';
|
||||||
|
export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE';
|
||||||
|
|
||||||
// eventually, handle errors more specifically and better
|
// eventually, handle errors more specifically and better
|
||||||
export const ERROR = 'ERROR';
|
export const ERROR = 'ERROR';
|
||||||
|
|
14
client/images/down-arrow.svg
Normal file
14
client/images/down-arrow.svg
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="14px" height="9px" viewBox="0 0 14 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>arrow shape copy 2</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#333333">
|
||||||
|
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
||||||
|
<polygon id="arrow-shape-copy-2" transform="translate(416.000000, 109.198314) rotate(-180.000000) translate(-416.000000, -109.198314) " points="417.4 106.396628 423 111.996628 421.6 113.396628 416 107.796628 410.4 113.396628 409 111.996628 414.6 106.396628 415.996628 105"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1,007 B |
12
client/images/left-arrow.svg
Normal file
12
client/images/left-arrow.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="10px" height="15px" viewBox="0 0 10 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>arrow shape copy</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-529.000000, -1165.000000)" fill="#333333">
|
||||||
|
<polygon id="arrow-shape-copy" transform="translate(534.000000, 1172.198314) rotate(-90.000000) translate(-534.000000, -1172.198314) " points="535.4 1169.39663 541 1174.99663 539.6 1176.39663 534 1170.79663 528.4 1176.39663 527 1174.99663 532.6 1169.39663 533.996628 1168"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 916 B |
16
client/images/plus-icon.svg
Normal file
16
client/images/plus-icon.svg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>close shape</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-558.000000, -1166.000000)" fill="#333333">
|
||||||
|
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
||||||
|
<g id="close-copy-3" transform="translate(499.500000, 110.000000) scale(1, -1) translate(-499.500000, -110.000000) translate(438.000000, 98.000000)">
|
||||||
|
<polygon id="close-shape" transform="translate(113.000000, 10.000000) rotate(45.000000) translate(-113.000000, -10.000000) " points="120 15.6 118.6 17 113 11.4 107.4 17 106 15.6 111.6 10 106 4.4 107.4 3 113 8.6 118.6 3 120 4.4 114.4 10"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
12
client/images/right-arrow.svg
Normal file
12
client/images/right-arrow.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="10px" height="15px" viewBox="0 0 10 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>arrow shape copy</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-496.000000, -1165.000000)" fill="#333333">
|
||||||
|
<polygon id="arrow-shape-copy" transform="translate(501.000000, 1172.198314) rotate(90.000000) translate(-501.000000, -1172.198314) " points="502.4 1169.39663 508 1174.99663 506.6 1176.39663 501 1170.79663 495.4 1176.39663 494 1174.99663 499.6 1169.39663 500.996628 1168"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 915 B |
14
client/images/up-arrow.svg
Normal file
14
client/images/up-arrow.svg
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="14px" height="9px" viewBox="0 0 14 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>arrow shape copy</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#333333">
|
||||||
|
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
||||||
|
<polygon id="arrow-shape-copy" points="386.4 106.396628 392 111.996628 390.6 113.396628 385 107.796628 379.4 113.396628 378 111.996628 383.6 106.396628 384.996628 105"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 900 B |
|
@ -1,4 +1,29 @@
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
|
import axios from 'axios';
|
||||||
|
import blobUtil from 'blob-util';
|
||||||
|
import xhr from 'xhr';
|
||||||
|
import fileType from 'file-type';
|
||||||
|
|
||||||
|
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
|
||||||
|
|
||||||
|
function appendToFilename(filename, string) {
|
||||||
|
const dotIndex = filename.lastIndexOf('.');
|
||||||
|
if (dotIndex === -1) return filename + string;
|
||||||
|
return filename.substring(0, dotIndex) + string + filename.substring(dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUniqueName(name, files) {
|
||||||
|
let testName = name;
|
||||||
|
let index = 1;
|
||||||
|
let existingName = files.find((file) => name === file.name);
|
||||||
|
|
||||||
|
while (existingName) {
|
||||||
|
testName = appendToFilename(name, `-${index}`);
|
||||||
|
index++;
|
||||||
|
existingName = files.find((file) => testName === file.name); // eslint-disable-line
|
||||||
|
}
|
||||||
|
return testName;
|
||||||
|
}
|
||||||
|
|
||||||
export function updateFileContent(name, content) {
|
export function updateFileContent(name, content) {
|
||||||
return {
|
return {
|
||||||
|
@ -7,3 +32,83 @@ export function updateFileContent(name, content) {
|
||||||
content
|
content
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBlobUrl(file) {
|
||||||
|
return (dispatch) => {
|
||||||
|
xhr({
|
||||||
|
uri: file.url,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
useXDR: true
|
||||||
|
}, (err, body, res) => {
|
||||||
|
if (err) throw err;
|
||||||
|
const typeOfFile = fileType(new Uint8Array(res));
|
||||||
|
blobUtil.arrayBufferToBlob(res, typeOfFile.mime)
|
||||||
|
.then(blobUtil.createObjectURL)
|
||||||
|
.then(objectURL => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.SET_BLOB_URL,
|
||||||
|
name: file.name,
|
||||||
|
blobURL: objectURL
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// blobUtil.imgSrcToBlob(file.url, undefined, { crossOrigin: 'Anonymous' })
|
||||||
|
// .then(blobUtil.createObjectURL)
|
||||||
|
// .then(objectURL => {
|
||||||
|
// dispatch({
|
||||||
|
// type: ActionTypes.SET_BLOB_URL,
|
||||||
|
// name: file.name,
|
||||||
|
// blobURL: objectURL
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFile(formProps) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
if (state.project.id) {
|
||||||
|
const postParams = {
|
||||||
|
name: createUniqueName(formProps.name, state.files),
|
||||||
|
url: formProps.url
|
||||||
|
};
|
||||||
|
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.url) {
|
||||||
|
getBlobUrl(response.data)(dispatch);
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.CREATE_FILE,
|
||||||
|
...response.data
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.HIDE_MODAL
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(response => dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
let maxFileId = 0;
|
||||||
|
state.files.forEach(file => {
|
||||||
|
if (parseInt(file.id, 10) > maxFileId) {
|
||||||
|
maxFileId = parseInt(file.id, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (formProps.url) {
|
||||||
|
getBlobUrl(formProps)(dispatch);
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.CREATE_FILE,
|
||||||
|
name: createUniqueName(formProps.name, state.files),
|
||||||
|
id: `${maxFileId + 1}`,
|
||||||
|
url: formProps.url
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.HIDE_MODAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -24,3 +24,48 @@ export function setSelectedFile(fileId) {
|
||||||
selectedFile: fileId
|
selectedFile: fileId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dispatchConsoleEvent(...args) {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.CONSOLE_EVENT,
|
||||||
|
event: args[0].data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newFile() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.SHOW_MODAL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeNewFileModal() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.HIDE_MODAL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandSidebar() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.EXPAND_SIDEBAR
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collapseSidebar() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.COLLAPSE_SIDEBAR
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandConsole() {
|
||||||
|
console.log('expand console!');
|
||||||
|
return {
|
||||||
|
type: ActionTypes.EXPAND_CONSOLE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collapseConsole() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.COLLAPSE_CONSOLE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,3 +23,43 @@ export function decreaseFont() {
|
||||||
type: ActionTypes.DECREASE_FONTSIZE
|
type: ActionTypes.DECREASE_FONTSIZE
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateFont(event) {
|
||||||
|
const value = event.target.value;
|
||||||
|
return {
|
||||||
|
type: ActionTypes.UPDATE_FONTSIZE,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function increaseIndentation() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.INCREASE_INDENTATION
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decreaseIndentation() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.DECREASE_INDENTATION
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateIndentation(event) {
|
||||||
|
const value = event.target.value;
|
||||||
|
return {
|
||||||
|
type: ActionTypes.UPDATE_INDENTATION,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function indentWithTab() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.INDENT_WITH_TAB
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function indentWithSpace() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.INDENT_WITH_SPACE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
import JSZipUtils from 'jszip-utils';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { getBlobUrl } from './files';
|
||||||
|
import async from 'async';
|
||||||
|
|
||||||
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
|
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
|
||||||
|
|
||||||
|
export function getProjectBlobUrls() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
state.files.forEach(file => {
|
||||||
|
if (file.url) {
|
||||||
|
getBlobUrl(file)(dispatch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getProject(id) {
|
export function getProject(id) {
|
||||||
return (dispatch) => {
|
return (dispatch, getState) => {
|
||||||
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
browserHistory.push(`/projects/${id}`);
|
browserHistory.push(`/projects/${id}`);
|
||||||
|
@ -13,8 +29,10 @@ export function getProject(id) {
|
||||||
type: ActionTypes.SET_PROJECT,
|
type: ActionTypes.SET_PROJECT,
|
||||||
project: response.data,
|
project: response.data,
|
||||||
files: response.data.files,
|
files: response.data.files,
|
||||||
selectedFile: response.data.selectedFile
|
selectedFile: response.data.selectedFile,
|
||||||
|
owner: response.data.user
|
||||||
});
|
});
|
||||||
|
getProjectBlobUrls()(dispatch, getState);
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch(response => dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
|
@ -61,6 +79,7 @@ export function saveProject() {
|
||||||
type: ActionTypes.NEW_PROJECT,
|
type: ActionTypes.NEW_PROJECT,
|
||||||
name: response.data.name,
|
name: response.data.name,
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
|
owner: response.data.user,
|
||||||
selectedFile: response.data.selectedFile,
|
selectedFile: response.data.selectedFile,
|
||||||
files: response.data.files
|
files: response.data.files
|
||||||
});
|
});
|
||||||
|
@ -83,6 +102,7 @@ export function createProject() {
|
||||||
type: ActionTypes.NEW_PROJECT,
|
type: ActionTypes.NEW_PROJECT,
|
||||||
name: response.data.name,
|
name: response.data.name,
|
||||||
id: response.data.id,
|
id: response.data.id,
|
||||||
|
owner: response.data.user,
|
||||||
selectedFile: response.data.selectedFile,
|
selectedFile: response.data.selectedFile,
|
||||||
files: response.data.files
|
files: response.data.files
|
||||||
});
|
});
|
||||||
|
@ -93,3 +113,52 @@ export function createProject() {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exportProjectAsZip() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
console.log('exporting project!');
|
||||||
|
const state = getState();
|
||||||
|
const zip = new JSZip();
|
||||||
|
async.each(state.files, (file, cb) => {
|
||||||
|
console.log(file);
|
||||||
|
if (file.url) {
|
||||||
|
JSZipUtils.getBinaryContent(file.url, (err, data) => {
|
||||||
|
zip.file(file.name, data, { binary: true });
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
zip.file(file.name, file.content);
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
if (err) console.log(err);
|
||||||
|
zip.generateAsync({ type: 'blob' }).then((content) => {
|
||||||
|
saveAs(content, `${state.project.name}.zip`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneProject() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const formParams = Object.assign({}, { name: state.project.name }, { files: state.files });
|
||||||
|
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
||||||
|
.then(response => {
|
||||||
|
browserHistory.push(`/projects/${response.data.id}`);
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.NEW_PROJECT,
|
||||||
|
name: response.data.name,
|
||||||
|
id: response.data.id,
|
||||||
|
owner: response.data.user,
|
||||||
|
selectedFile: response.data.selectedFile,
|
||||||
|
files: response.data.files
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(response => dispatch({
|
||||||
|
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||||
|
error: response.data
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
65
client/modules/IDE/actions/uploader.js
Normal file
65
client/modules/IDE/actions/uploader.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { createFile } from './files';
|
||||||
|
|
||||||
|
const s3Bucket = `http://${process.env.S3_BUCKET}.s3.amazonaws.com/`;
|
||||||
|
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
|
||||||
|
|
||||||
|
export function dropzoneAcceptCallback(file, done) {
|
||||||
|
return () => {
|
||||||
|
file.postData = []; // eslint-disable-line
|
||||||
|
axios.post(`${ROOT_URL}/S3/sign`, {
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
// _csrf: document.getElementById('__createPostToken').value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
file.custom_status = 'ready'; // eslint-disable-line
|
||||||
|
file.postData = response.data; // eslint-disable-line
|
||||||
|
file.s3 = response.data.key; // eslint-disable-line
|
||||||
|
file.previewTemplate.className += ' uploading'; // eslint-disable-line
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(response => {
|
||||||
|
file.custom_status = 'rejected'; // eslint-disable-line
|
||||||
|
if (response.data.responseText && response.data.responseText.message) {
|
||||||
|
done(response.data.responseText.message);
|
||||||
|
}
|
||||||
|
done('error preparing the upload');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dropzoneSendingCallback(file, xhr, formData) {
|
||||||
|
return () => {
|
||||||
|
Object.keys(file.postData).forEach(key => {
|
||||||
|
formData.append(key, file.postData[key]);
|
||||||
|
});
|
||||||
|
formData.append('Content-type', '');
|
||||||
|
formData.append('Content-length', '');
|
||||||
|
formData.append('acl', 'public-read');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dropzoneCompleteCallback(file) {
|
||||||
|
return (dispatch, getState) => { // eslint-disable-line
|
||||||
|
let inputHidden = '<input type="hidden" name="attachments[]" value="';
|
||||||
|
const json = {
|
||||||
|
url: `${s3Bucket}${file.postData.key}`,
|
||||||
|
originalFilename: file.name
|
||||||
|
};
|
||||||
|
console.log(json, JSON.stringify(json), JSON.stringify(json).replace('"', '\\"'));
|
||||||
|
inputHidden += `${window.btoa(JSON.stringify(json))}" />`;
|
||||||
|
// document.getElementById('uploader').appendChild(inputHidden);
|
||||||
|
document.getElementById('uploader').innerHTML += inputHidden;
|
||||||
|
|
||||||
|
const formParams = {
|
||||||
|
name: file.name,
|
||||||
|
url: `${s3Bucket}${file.postData.key}`
|
||||||
|
};
|
||||||
|
createFile(formParams)(dispatch, getState);
|
||||||
|
};
|
||||||
|
}
|
85
client/modules/IDE/components/Console.js
Normal file
85
client/modules/IDE/components/Console.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
const upArrowUrl = require('../../../images/up-arrow.svg');
|
||||||
|
const downArrowUrl = require('../../../images/down-arrow.svg');
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many console messages to store
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
const consoleMax = 100;
|
||||||
|
|
||||||
|
class Console extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of React Elements that include previous console messages
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
this.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.isPlaying && !this.props.isPlaying) {
|
||||||
|
this.children = [];
|
||||||
|
} else if (nextProps.consoleEvent !== this.props.consoleEvent) {
|
||||||
|
const args = nextProps.consoleEvent.arguments;
|
||||||
|
const method = nextProps.consoleEvent.method;
|
||||||
|
const nextChild = (
|
||||||
|
<div key={this.children.length} className={`preview-console__${method}`}>
|
||||||
|
{Object.keys(args).map((key) => <span key={`${this.children.length}-${key}`}>{args[key]}</span>)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
this.children.push(nextChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
return (nextProps.consoleEvent !== this.props.consoleEvent)
|
||||||
|
|| (nextProps.isPlaying && !this.props.isPlaying)
|
||||||
|
|| (this.props.isExpanded !== nextProps.isExpanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.refs.console_messages.scrollTop = this.refs.console_messages.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const childrenToDisplay = this.children.slice(-consoleMax);
|
||||||
|
const consoleClass = classNames({
|
||||||
|
'preview-console': true,
|
||||||
|
'preview-console--collapsed': !this.props.isExpanded
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref="console" className={consoleClass}>
|
||||||
|
<div className="preview-console__header">
|
||||||
|
<h2 className="preview-console__header-title">console</h2>
|
||||||
|
<a className="preview-console__collapse" onClick={this.props.collapseConsole} >
|
||||||
|
<InlineSVG src={downArrowUrl} />
|
||||||
|
</a>
|
||||||
|
<a className="preview-console__expand" onClick={this.props.expandConsole} >
|
||||||
|
<InlineSVG src={upArrowUrl} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div ref="console_messages" className="preview-console__messages">
|
||||||
|
{childrenToDisplay}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.propTypes = {
|
||||||
|
consoleEvent: PropTypes.object,
|
||||||
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
|
isExpanded: PropTypes.bool.isRequired,
|
||||||
|
collapseConsole: PropTypes.func.isRequired,
|
||||||
|
expandConsole: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Console;
|
|
@ -1,7 +1,23 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import CodeMirror from 'codemirror';
|
import CodeMirror from 'codemirror';
|
||||||
import 'codemirror/mode/javascript/javascript';
|
import 'codemirror/mode/javascript/javascript';
|
||||||
|
import 'codemirror/mode/css/css';
|
||||||
|
import 'codemirror/mode/htmlmixed/htmlmixed';
|
||||||
import 'codemirror/addon/selection/active-line';
|
import 'codemirror/addon/selection/active-line';
|
||||||
|
import 'codemirror/addon/lint/lint';
|
||||||
|
import 'codemirror/addon/lint/javascript-lint';
|
||||||
|
import 'codemirror/addon/lint/css-lint';
|
||||||
|
import 'codemirror/addon/lint/html-lint';
|
||||||
|
import 'codemirror/addon/comment/comment';
|
||||||
|
import 'codemirror/keymap/sublime';
|
||||||
|
import { JSHINT } from 'jshint';
|
||||||
|
window.JSHINT = JSHINT;
|
||||||
|
import { CSSLint } from 'csslint';
|
||||||
|
window.CSSLint = CSSLint;
|
||||||
|
import { HTMLHint } from 'htmlhint';
|
||||||
|
window.HTMLHint = HTMLHint;
|
||||||
|
|
||||||
|
import { debounce } from 'throttle-debounce';
|
||||||
|
|
||||||
class Editor extends React.Component {
|
class Editor extends React.Component {
|
||||||
|
|
||||||
|
@ -11,13 +27,24 @@ class Editor extends React.Component {
|
||||||
value: this.props.file.content,
|
value: this.props.file.content,
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
styleActiveLine: true,
|
styleActiveLine: true,
|
||||||
mode: 'javascript'
|
inputStyle: 'contenteditable',
|
||||||
|
mode: 'javascript',
|
||||||
|
lineWrapping: true,
|
||||||
|
gutters: ['CodeMirror-lint-markers'],
|
||||||
|
lint: true,
|
||||||
|
keyMap: 'sublime'
|
||||||
});
|
});
|
||||||
this._cm.on('change', () => { // eslint-disable-line
|
this._cm.on('change', debounce(200, () => {
|
||||||
// this.props.updateFileContent('sketch.js', this._cm.getValue());
|
|
||||||
this.props.updateFileContent(this.props.file.name, this._cm.getValue());
|
this.props.updateFileContent(this.props.file.name, this._cm.getValue());
|
||||||
});
|
}));
|
||||||
|
// this._cm.on('change', () => { // eslint-disable-line
|
||||||
|
// // this.props.updateFileContent('sketch.js', this._cm.getValue());
|
||||||
|
// throttle(1000, () => console.log('debounce is working!'));
|
||||||
|
// this.props.updateFileContent(this.props.file.name, this._cm.getValue());
|
||||||
|
// });
|
||||||
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
|
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
|
||||||
|
this._cm.setOption('indentWithTabs', this.props.isTabIndent);
|
||||||
|
this._cm.setOption('tabSize', this.props.indentationAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -28,6 +55,21 @@ class Editor extends React.Component {
|
||||||
if (this.props.fontSize !== prevProps.fontSize) {
|
if (this.props.fontSize !== prevProps.fontSize) {
|
||||||
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
|
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
|
||||||
}
|
}
|
||||||
|
if (this.props.indentationAmount !== prevProps.indentationAmount) {
|
||||||
|
this._cm.setOption('tabSize', this.props.indentationAmount);
|
||||||
|
}
|
||||||
|
if (this.props.isTabIndent !== prevProps.isTabIndent) {
|
||||||
|
this._cm.setOption('indentWithTabs', this.props.isTabIndent);
|
||||||
|
}
|
||||||
|
if (this.props.file.name !== prevProps.name) {
|
||||||
|
if (this.props.file.name.match(/.+\.js$/)) {
|
||||||
|
this._cm.setOption('mode', 'javascript');
|
||||||
|
} else if (this.props.file.name.match(/.+\.css$/)) {
|
||||||
|
this._cm.setOption('mode', 'css');
|
||||||
|
} else if (this.props.file.name.match(/.+\.html$/)) {
|
||||||
|
this._cm.setOption('mode', 'htmlmixed');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -37,17 +79,19 @@ class Editor extends React.Component {
|
||||||
_cm: CodeMirror.Editor
|
_cm: CodeMirror.Editor
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div ref="container" className="editor-holder"></div>;
|
return <div ref="container" className="editor-holder" tabIndex="0" title="code editor" role="region"></div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Editor.propTypes = {
|
Editor.propTypes = {
|
||||||
|
indentationAmount: PropTypes.number.isRequired,
|
||||||
|
isTabIndent: PropTypes.bool.isRequired,
|
||||||
|
updateFileContent: PropTypes.func.isRequired,
|
||||||
|
fontSize: PropTypes.number.isRequired,
|
||||||
file: PropTypes.shape({
|
file: PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
content: PropTypes.string.isRequired
|
content: PropTypes.string.isRequired
|
||||||
}),
|
})
|
||||||
updateFileContent: PropTypes.func.isRequired,
|
|
||||||
fontSize: PropTypes.number.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
|
58
client/modules/IDE/components/FileUploader.js
Normal file
58
client/modules/IDE/components/FileUploader.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import Dropzone from 'dropzone';
|
||||||
|
const s3Bucket = `http://${process.env.S3_BUCKET}.s3.amazonaws.com/`;
|
||||||
|
import * as UploaderActions from '../actions/uploader';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
class FileUploader extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
this.createDropzone();
|
||||||
|
Dropzone.autoDiscover = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
createDropzone() {
|
||||||
|
this.uploader = new Dropzone('div#uploader', {
|
||||||
|
url: s3Bucket,
|
||||||
|
method: 'post',
|
||||||
|
autoProcessQueue: true,
|
||||||
|
clickable: true,
|
||||||
|
maxFiles: 1,
|
||||||
|
parallelUploads: 1,
|
||||||
|
maxFilesize: 10, // in mb
|
||||||
|
maxThumbnailFilesize: 8, // 3MB
|
||||||
|
thumbnailWidth: 200,
|
||||||
|
thumbnailHeight: 200,
|
||||||
|
acceptedFiles: 'image/bmp,image/gif,image/jpg,image/jpeg,image/png,audio/*',
|
||||||
|
dictDefaultMessage: 'Drop files here to upload or click to use the file browser',
|
||||||
|
accept: this.props.dropzoneAcceptCallback,
|
||||||
|
sending: this.props.dropzoneSendingCallback,
|
||||||
|
complete: this.props.dropzoneCompleteCallback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="uploader" className="uploader dropzone"></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUploader.propTypes = {
|
||||||
|
dropzoneAcceptCallback: PropTypes.func.isRequired,
|
||||||
|
dropzoneSendingCallback: PropTypes.func.isRequired,
|
||||||
|
dropzoneCompleteCallback: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
files: state.files,
|
||||||
|
project: state.project
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(UploaderActions, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(FileUploader);
|
28
client/modules/IDE/components/NewFileForm.js
Normal file
28
client/modules/IDE/components/NewFileForm.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
|
function NewFileForm(props) {
|
||||||
|
const { fields: { name }, handleSubmit } = props;
|
||||||
|
return (
|
||||||
|
<form className="new-file-form" onSubmit={handleSubmit(props.createFile.bind(this))}>
|
||||||
|
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
|
||||||
|
<input
|
||||||
|
className="new-file-form__name-input"
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
{...name}
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Add File" />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NewFileForm.propTypes = {
|
||||||
|
fields: PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
createFile: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewFileForm;
|
79
client/modules/IDE/components/NewFileModal.js
Normal file
79
client/modules/IDE/components/NewFileModal.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import NewFileForm from './NewFileForm';
|
||||||
|
import * as FileActions from '../actions/files';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
const exitUrl = require('../../../images/exit.svg');
|
||||||
|
|
||||||
|
import FileUploader from './FileUploader';
|
||||||
|
|
||||||
|
// At some point this will probably be generalized to a generic modal
|
||||||
|
// in which you can insert different content
|
||||||
|
// but for now, let's just make this work
|
||||||
|
function NewFileModal(props) {
|
||||||
|
const modalClass = classNames({
|
||||||
|
modal: true,
|
||||||
|
'modal--reduced': !props.canUploadMedia
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={modalClass}>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal__header">
|
||||||
|
<h2 className="modal__title">Add File</h2>
|
||||||
|
<button className="modal__exit-button" onClick={props.closeModal}>
|
||||||
|
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<NewFileForm {...props} />
|
||||||
|
{(() => {
|
||||||
|
if (props.canUploadMedia) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="modal__divider">OR</p>
|
||||||
|
<FileUploader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NewFileModal.propTypes = {
|
||||||
|
closeModal: PropTypes.func.isRequired,
|
||||||
|
canUploadMedia: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
file: state.files
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(FileActions, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(formProps) {
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
|
if (!formProps.name) {
|
||||||
|
errors.name = 'Please enter a name';
|
||||||
|
} else if (!formProps.name.match(/(.+\.js$|.+\.css$)/)) {
|
||||||
|
errors.name = 'File must be of type JavaScript or CSS.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default reduxForm({
|
||||||
|
form: 'new-file',
|
||||||
|
fields: ['name'],
|
||||||
|
validate
|
||||||
|
}, mapStateToProps, mapDispatchToProps)(NewFileModal);
|
|
@ -11,25 +11,101 @@ function Preferences(props) {
|
||||||
preferences: true,
|
preferences: true,
|
||||||
'preferences--selected': props.isVisible
|
'preferences--selected': props.isVisible
|
||||||
});
|
});
|
||||||
|
let preferencesTabOptionClass = classNames({
|
||||||
|
preference__option: true,
|
||||||
|
'preference__option--selected': props.isTabIndent
|
||||||
|
});
|
||||||
|
let preferencesSpaceOptionClass = classNames({
|
||||||
|
preference__option: true,
|
||||||
|
'preference__option--selected': !props.isTabIndent
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className={preferencesContainerClass} tabIndex="0">
|
<section className={preferencesContainerClass} tabIndex="0" title="preference-menu">
|
||||||
<div className="preferences__heading">
|
<div className="preferences__heading">
|
||||||
<h2 className="preferences__title">Preferences</h2>
|
<h2 className="preferences__title">Preferences</h2>
|
||||||
<button className="preferences__exit-button" onClick={props.closePreferences}>
|
<button
|
||||||
|
className="preferences__exit-button"
|
||||||
|
onClick={props.closePreferences}
|
||||||
|
title="exit"
|
||||||
|
>
|
||||||
<Isvg src={exitUrl} alt="Exit Preferences" />
|
<Isvg src={exitUrl} alt="Exit Preferences" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h3 className="preference__title">Text Size</h3>
|
<h4 className="preference__title">Text Size</h4>
|
||||||
<button className="preference__plus-button" onClick={props.decreaseFont}>
|
<button
|
||||||
|
className="preference__plus-button"
|
||||||
|
onClick={props.decreaseFont}
|
||||||
|
id="preference-decrease-font-size"
|
||||||
|
>
|
||||||
<Isvg src={minusUrl} alt="Decrease Font Size" />
|
<Isvg src={minusUrl} alt="Decrease Font Size" />
|
||||||
|
<h6 className="preference__label">Decrease</h6>
|
||||||
</button>
|
</button>
|
||||||
<p className="preference__value">{props.fontSize}</p>
|
<label htmlFor="preference-decrease-font-size" className="preference__button-label">
|
||||||
<button className="preference__minus-button" onClick={props.increaseFont}>
|
Decrease Font Size
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="preference__value"
|
||||||
|
aria-live="status"
|
||||||
|
aria-live="polite"
|
||||||
|
role="status"
|
||||||
|
value={props.fontSize}
|
||||||
|
onChange={props.updateFont}
|
||||||
|
>
|
||||||
|
</input>
|
||||||
|
<button
|
||||||
|
className="preference__minus-button"
|
||||||
|
onClick={props.increaseFont}
|
||||||
|
id="preference-increase-font-size"
|
||||||
|
>
|
||||||
<Isvg src={plusUrl} alt="Increase Font Size" />
|
<Isvg src={plusUrl} alt="Increase Font Size" />
|
||||||
|
<h6 className="preference__label">Increase</h6>
|
||||||
</button>
|
</button>
|
||||||
|
<label htmlFor="preference-increase-font-size" className="preference__button-label">
|
||||||
|
Increase Font Size
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="preference">
|
||||||
|
<h4 className="preference__title">Indentation Amount</h4>
|
||||||
|
<button
|
||||||
|
className="preference__plus-button"
|
||||||
|
onClick={props.decreaseIndentation}
|
||||||
|
id="preference-decrease-indentation"
|
||||||
|
>
|
||||||
|
<Isvg src={minusUrl} alt="DecreaseIndentation Amount" />
|
||||||
|
<h6 className="preference__label">Decrease</h6>
|
||||||
|
</button>
|
||||||
|
<label htmlFor="preference-decrease-indentation" className="preference__button-label">
|
||||||
|
Decrease Indentation Amount
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="preference__value"
|
||||||
|
aria-live="status"
|
||||||
|
aria-live="polite"
|
||||||
|
role="status"
|
||||||
|
value={props.indentationAmount}
|
||||||
|
onChange={props.updateIndentation}
|
||||||
|
>
|
||||||
|
</input>
|
||||||
|
<button
|
||||||
|
className="preference__minus-button"
|
||||||
|
onClick={props.increaseIndentation}
|
||||||
|
id="preference-increase-indentation"
|
||||||
|
>
|
||||||
|
<Isvg src={plusUrl} alt="IncreaseIndentation Amount" />
|
||||||
|
<h6 className="preference__label">Increase</h6>
|
||||||
|
</button>
|
||||||
|
<label htmlFor="preference-increase-indentation" className="preference__button-label">
|
||||||
|
Increase Indentation Amount
|
||||||
|
</label>
|
||||||
|
<div className="preference__vertical-list">
|
||||||
|
<button className={preferencesSpaceOptionClass} onClick={props.indentWithSpace}>Spaces</button>
|
||||||
|
<button className={preferencesTabOptionClass} onClick={props.indentWithTab}>Tabs</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +113,16 @@ Preferences.propTypes = {
|
||||||
isVisible: PropTypes.bool.isRequired,
|
isVisible: PropTypes.bool.isRequired,
|
||||||
closePreferences: PropTypes.func.isRequired,
|
closePreferences: PropTypes.func.isRequired,
|
||||||
decreaseFont: PropTypes.func.isRequired,
|
decreaseFont: PropTypes.func.isRequired,
|
||||||
|
updateFont: PropTypes.func.isRequired,
|
||||||
fontSize: PropTypes.number.isRequired,
|
fontSize: PropTypes.number.isRequired,
|
||||||
increaseFont: PropTypes.func.isRequired
|
increaseFont: PropTypes.func.isRequired,
|
||||||
|
indentationAmount: PropTypes.number.isRequired,
|
||||||
|
decreaseIndentation: PropTypes.func.isRequired,
|
||||||
|
increaseIndentation: PropTypes.func.isRequired,
|
||||||
|
updateIndentation: PropTypes.func.isRequired,
|
||||||
|
indentWithSpace: PropTypes.func.isRequired,
|
||||||
|
indentWithTab: PropTypes.func.isRequired,
|
||||||
|
isTabIndent: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Preferences;
|
export default Preferences;
|
||||||
|
|
|
@ -3,12 +3,68 @@ import ReactDOM from 'react-dom';
|
||||||
import escapeStringRegexp from 'escape-string-regexp';
|
import escapeStringRegexp from 'escape-string-regexp';
|
||||||
import srcDoc from 'srcdoc-polyfill';
|
import srcDoc from 'srcdoc-polyfill';
|
||||||
|
|
||||||
|
const hijackConsoleScript = `<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var iframeWindow = window;
|
||||||
|
var originalConsole = iframeWindow.console;
|
||||||
|
iframeWindow.console = {};
|
||||||
|
|
||||||
|
var methods = [
|
||||||
|
'debug', 'clear', 'error', 'info', 'log', 'warn'
|
||||||
|
];
|
||||||
|
|
||||||
|
methods.forEach( function(method) {
|
||||||
|
iframeWindow.console[method] = function() {
|
||||||
|
originalConsole[method].apply(originalConsole, arguments);
|
||||||
|
|
||||||
|
var args = Array.from(arguments);
|
||||||
|
args = args.map(function(i) {
|
||||||
|
// catch objects
|
||||||
|
return (typeof i === 'string') ? i : JSON.stringify(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// post message to parent window
|
||||||
|
window.parent.postMessage({
|
||||||
|
method: method,
|
||||||
|
arguments: args,
|
||||||
|
source: 'sketch'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// catch reference errors, via http://stackoverflow.com/a/12747364/2994108
|
||||||
|
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||||
|
var string = msg.toLowerCase();
|
||||||
|
var substring = "script error";
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
if (string.indexOf(substring) > -1){
|
||||||
|
data = 'Script Error: See Browser Console for Detail';
|
||||||
|
} else {
|
||||||
|
data = msg + ' Line: ' + lineNo + 'column: ' + columnNo;
|
||||||
|
}
|
||||||
|
window.parent.postMessage({
|
||||||
|
method: 'error',
|
||||||
|
arguments: data,
|
||||||
|
source: 'sketch'
|
||||||
|
}, '*');
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>`;
|
||||||
|
|
||||||
class PreviewFrame extends React.Component {
|
class PreviewFrame extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.isPlaying) {
|
if (this.props.isPlaying) {
|
||||||
this.renderFrameContents();
|
this.renderFrameContents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', (msg) => {
|
||||||
|
if (msg.data.source === 'sketch') {
|
||||||
|
this.props.dispatchConsoleEvent(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -33,7 +89,33 @@ class PreviewFrame extends React.Component {
|
||||||
injectLocalFiles() {
|
injectLocalFiles() {
|
||||||
let htmlFile = this.props.htmlFile.content;
|
let htmlFile = this.props.htmlFile.content;
|
||||||
|
|
||||||
|
// have to build the array manually because the spread operator is only
|
||||||
|
// one level down...
|
||||||
|
const jsFiles = [];
|
||||||
this.props.jsFiles.forEach(jsFile => {
|
this.props.jsFiles.forEach(jsFile => {
|
||||||
|
const newJSFile = { ...jsFile };
|
||||||
|
let jsFileStrings = newJSFile.content.match(/(['"])((\\\1|.)*?)\1/gm);
|
||||||
|
jsFileStrings = jsFileStrings || [];
|
||||||
|
jsFileStrings.forEach(jsFileString => {
|
||||||
|
if (jsFileString.match(/^('|")(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg)('|")$/)) {
|
||||||
|
const filePath = jsFileString.substr(1, jsFileString.length - 2);
|
||||||
|
let fileName = filePath;
|
||||||
|
if (fileName.match(/^\.\//)) {
|
||||||
|
fileName = fileName.substr(2, fileName.length - 1);
|
||||||
|
} else if (fileName.match(/^\//)) {
|
||||||
|
fileName = fileName.substr(1, fileName.length - 1);
|
||||||
|
}
|
||||||
|
this.props.files.forEach(file => {
|
||||||
|
if (file.name === fileName) {
|
||||||
|
newJSFile.content = newJSFile.content.replace(filePath, file.blobURL); // eslint-disable-line
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jsFiles.push(newJSFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
jsFiles.forEach(jsFile => {
|
||||||
const fileName = escapeStringRegexp(jsFile.name);
|
const fileName = escapeStringRegexp(jsFile.name);
|
||||||
const fileRegex = new RegExp(`<script.*?src=('|")((\.\/)|\/)?${fileName}('|").*?>([\s\S]*?)<\/script>`, 'gmi');
|
const fileRegex = new RegExp(`<script.*?src=('|")((\.\/)|\/)?${fileName}('|").*?>([\s\S]*?)<\/script>`, 'gmi');
|
||||||
htmlFile = htmlFile.replace(fileRegex, `<script>\n${jsFile.content}\n</script>`);
|
htmlFile = htmlFile.replace(fileRegex, `<script>\n${jsFile.content}\n</script>`);
|
||||||
|
@ -51,6 +133,7 @@ class PreviewFrame extends React.Component {
|
||||||
// htmlHeadContents = htmlHeadContents.slice(1, htmlHeadContents.length - 2);
|
// htmlHeadContents = htmlHeadContents.slice(1, htmlHeadContents.length - 2);
|
||||||
// htmlHeadContents += '<link rel="stylesheet" type="text/css" href="/preview-styles.css" />\n';
|
// htmlHeadContents += '<link rel="stylesheet" type="text/css" href="/preview-styles.css" />\n';
|
||||||
// htmlFile = htmlFile.replace(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi, `<head>\n${htmlHeadContents}\n</head>`);
|
// htmlFile = htmlFile.replace(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi, `<head>\n${htmlHeadContents}\n</head>`);
|
||||||
|
htmlFile += hijackConsoleScript;
|
||||||
|
|
||||||
return htmlFile;
|
return htmlFile;
|
||||||
}
|
}
|
||||||
|
@ -58,15 +141,10 @@ class PreviewFrame extends React.Component {
|
||||||
renderSketch() {
|
renderSketch() {
|
||||||
const doc = ReactDOM.findDOMNode(this);
|
const doc = ReactDOM.findDOMNode(this);
|
||||||
if (this.props.isPlaying) {
|
if (this.props.isPlaying) {
|
||||||
// TODO add polyfill for this
|
|
||||||
// doc.srcdoc = this.injectLocalFiles();
|
|
||||||
srcDoc.set(doc, this.injectLocalFiles());
|
srcDoc.set(doc, this.injectLocalFiles());
|
||||||
} else {
|
} else {
|
||||||
// doc.srcdoc = '';
|
doc.srcdoc = '';
|
||||||
srcDoc.set(doc, ' ');
|
srcDoc.set(doc, ' ');
|
||||||
doc.contentWindow.document.open();
|
|
||||||
doc.contentWindow.document.write('');
|
|
||||||
doc.contentWindow.document.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,10 +161,12 @@ class PreviewFrame extends React.Component {
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
className="preview-frame"
|
className="preview-frame"
|
||||||
|
role="region"
|
||||||
|
tabIndex="0"
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
title="sketch output"
|
title="sketch output"
|
||||||
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
|
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
|
||||||
></iframe>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +179,10 @@ PreviewFrame.propTypes = {
|
||||||
content: PropTypes.string.isRequired
|
content: PropTypes.string.isRequired
|
||||||
}),
|
}),
|
||||||
jsFiles: PropTypes.array.isRequired,
|
jsFiles: PropTypes.array.isRequired,
|
||||||
cssFiles: PropTypes.array.isRequired
|
cssFiles: PropTypes.array.isRequired,
|
||||||
|
files: PropTypes.array.isRequired,
|
||||||
|
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||||
|
children: PropTypes.element
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PreviewFrame;
|
export default PreviewFrame;
|
||||||
|
|
|
@ -1,9 +1,40 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
const rightArrowUrl = require('../../../images/right-arrow.svg');
|
||||||
|
const leftArrowUrl = require('../../../images/left-arrow.svg');
|
||||||
|
|
||||||
function Sidebar(props) {
|
function Sidebar(props) {
|
||||||
|
const sidebarClass = classNames({
|
||||||
|
sidebar: true,
|
||||||
|
'sidebar--contracted': !props.isExpanded
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="sidebar">
|
<nav className={sidebarClass} title="file-navigation" role="navigation">
|
||||||
|
<div className="sidebar__header">
|
||||||
|
<h3 className="sidebar__title">Sketch Files</h3>
|
||||||
|
<div className="sidebar__icons">
|
||||||
|
<a
|
||||||
|
className="sidebar__add"
|
||||||
|
onClick={props.newFile}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="sidebar__contract"
|
||||||
|
onClick={props.collapseSidebar}
|
||||||
|
>
|
||||||
|
<InlineSVG src={leftArrowUrl} />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="sidebar__expand"
|
||||||
|
onClick={props.expandSidebar}
|
||||||
|
>
|
||||||
|
<InlineSVG src={rightArrowUrl} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ul className="sidebar__file-list">
|
<ul className="sidebar__file-list">
|
||||||
{props.files.map(file => {
|
{props.files.map(file => {
|
||||||
let itemClass = classNames({
|
let itemClass = classNames({
|
||||||
|
@ -14,12 +45,15 @@ function Sidebar(props) {
|
||||||
<li
|
<li
|
||||||
className={itemClass}
|
className={itemClass}
|
||||||
key={file.id}
|
key={file.id}
|
||||||
|
>
|
||||||
|
<a
|
||||||
onClick={() => props.setSelectedFile(file.id)}
|
onClick={() => props.setSelectedFile(file.id)}
|
||||||
>{file.name}</li>
|
>{file.name}</a>
|
||||||
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
const InlineSVG = require('react-inlinesvg');
|
||||||
const Isvg = require('react-inlinesvg');
|
|
||||||
const playUrl = require('../../../images/play.svg');
|
const playUrl = require('../../../images/play.svg');
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
const logoUrl = require('../../../images/p5js-logo.svg');
|
||||||
const stopUrl = require('../../../images/stop.svg');
|
const stopUrl = require('../../../images/stop.svg');
|
||||||
const preferencesUrl = require('../../../images/preferences.svg');
|
const preferencesUrl = require('../../../images/preferences.svg');
|
||||||
const classNames = require('classnames');
|
import classNames from 'classnames';
|
||||||
|
|
||||||
function Toolbar(props) {
|
function Toolbar(props) {
|
||||||
let playButtonClass = classNames({
|
let playButtonClass = classNames({
|
||||||
|
@ -24,12 +23,18 @@ function Toolbar(props) {
|
||||||
return (
|
return (
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<img className="toolbar__logo" src={logoUrl} alt="p5js Logo" />
|
<img className="toolbar__logo" src={logoUrl} alt="p5js Logo" />
|
||||||
<button className={playButtonClass} onClick={props.startSketch}>
|
<button className={playButtonClass} onClick={props.startSketch} id="play-button">
|
||||||
<Isvg src={playUrl} alt="Play Sketch" />
|
<InlineSVG src={playUrl} alt="Play Sketch" />
|
||||||
</button>
|
</button>
|
||||||
<button className={stopButtonClass} onClick={props.stopSketch}>
|
<label htmlFor="play-button" className="toolbar__button-label">
|
||||||
<Isvg src={stopUrl} alt="Stop Sketch" />
|
play
|
||||||
|
</label>
|
||||||
|
<button className={stopButtonClass} onClick={props.stopSketch} id="stop-button">
|
||||||
|
<InlineSVG src={stopUrl} alt="Stop Sketch" />
|
||||||
</button>
|
</button>
|
||||||
|
<label htmlFor="stop-button" className="toolbar__button-label">
|
||||||
|
stop
|
||||||
|
</label>
|
||||||
<div className="toolbar__project-name-container">
|
<div className="toolbar__project-name-container">
|
||||||
<span
|
<span
|
||||||
className="toolbar__project-name"
|
className="toolbar__project-name"
|
||||||
|
@ -40,10 +45,24 @@ function Toolbar(props) {
|
||||||
>
|
>
|
||||||
{props.projectName}
|
{props.projectName}
|
||||||
</span>
|
</span>
|
||||||
|
{(() => { // eslint-disable-line
|
||||||
|
if (props.owner) {
|
||||||
|
return (
|
||||||
|
<p className="toolbar__project-owner">by <span>{props.owner.username}</span></p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<button className={preferencesButtonClass} onClick={props.openPreferences}>
|
<button
|
||||||
<Isvg src={preferencesUrl} alt="Show Preferences" />
|
className={preferencesButtonClass}
|
||||||
|
onClick={props.openPreferences}
|
||||||
|
id="preferences-button"
|
||||||
|
>
|
||||||
|
<InlineSVG src={preferencesUrl} alt="Show Preferences" />
|
||||||
</button>
|
</button>
|
||||||
|
<label htmlFor="preferences-button" className="toolbar__button-label">
|
||||||
|
preferences
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +74,10 @@ Toolbar.propTypes = {
|
||||||
stopSketch: PropTypes.func.isRequired,
|
stopSketch: PropTypes.func.isRequired,
|
||||||
setProjectName: PropTypes.func.isRequired,
|
setProjectName: PropTypes.func.isRequired,
|
||||||
projectName: PropTypes.string.isRequired,
|
projectName: PropTypes.string.isRequired,
|
||||||
openPreferences: PropTypes.func.isRequired
|
openPreferences: PropTypes.func.isRequired,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
username: PropTypes.string
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Toolbar;
|
export default Toolbar;
|
||||||
|
|
|
@ -4,7 +4,9 @@ import Sidebar from '../components/Sidebar';
|
||||||
import PreviewFrame from '../components/PreviewFrame';
|
import PreviewFrame from '../components/PreviewFrame';
|
||||||
import Toolbar from '../components/Toolbar';
|
import Toolbar from '../components/Toolbar';
|
||||||
import Preferences from '../components/Preferences';
|
import Preferences from '../components/Preferences';
|
||||||
|
import NewFileModal from '../components/NewFileModal';
|
||||||
import Nav from '../../../components/Nav';
|
import Nav from '../../../components/Nav';
|
||||||
|
import Console from '../components/Console';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as FileActions from '../actions/files';
|
import * as FileActions from '../actions/files';
|
||||||
|
@ -28,6 +30,8 @@ class IDEView extends React.Component {
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
createProject={this.props.createProject}
|
createProject={this.props.createProject}
|
||||||
saveProject={this.props.saveProject}
|
saveProject={this.props.saveProject}
|
||||||
|
exportProjectAsZip={this.props.exportProjectAsZip}
|
||||||
|
cloneProject={this.props.cloneProject}
|
||||||
/>
|
/>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
className="Toolbar"
|
className="Toolbar"
|
||||||
|
@ -38,25 +42,50 @@ class IDEView extends React.Component {
|
||||||
setProjectName={this.props.setProjectName}
|
setProjectName={this.props.setProjectName}
|
||||||
openPreferences={this.props.openPreferences}
|
openPreferences={this.props.openPreferences}
|
||||||
isPreferencesVisible={this.props.preferences.isVisible}
|
isPreferencesVisible={this.props.preferences.isVisible}
|
||||||
|
owner={this.props.project.owner}
|
||||||
/>
|
/>
|
||||||
<Preferences
|
<Preferences
|
||||||
isVisible={this.props.preferences.isVisible}
|
isVisible={this.props.preferences.isVisible}
|
||||||
closePreferences={this.props.closePreferences}
|
closePreferences={this.props.closePreferences}
|
||||||
increaseFont={this.props.increaseFont}
|
increaseFont={this.props.increaseFont}
|
||||||
decreaseFont={this.props.decreaseFont}
|
decreaseFont={this.props.decreaseFont}
|
||||||
|
updateFont={this.props.updateFont}
|
||||||
fontSize={this.props.preferences.fontSize}
|
fontSize={this.props.preferences.fontSize}
|
||||||
|
increaseIndentation={this.props.increaseIndentation}
|
||||||
|
decreaseIndentation={this.props.decreaseIndentation}
|
||||||
|
updateIndentation={this.props.updateIndentation}
|
||||||
|
indentationAmount={this.props.preferences.indentationAmount}
|
||||||
|
isTabIndent={this.props.preferences.isTabIndent}
|
||||||
|
indentWithSpace={this.props.indentWithSpace}
|
||||||
|
indentWithTab={this.props.indentWithTab}
|
||||||
/>
|
/>
|
||||||
|
<div className="editor-preview-container">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
files={this.props.files}
|
files={this.props.files}
|
||||||
selectedFile={this.props.selectedFile}
|
selectedFile={this.props.selectedFile}
|
||||||
setSelectedFile={this.props.setSelectedFile}
|
setSelectedFile={this.props.setSelectedFile}
|
||||||
|
newFile={this.props.newFile}
|
||||||
|
isExpanded={this.props.ide.sidebarIsExpanded}
|
||||||
|
expandSidebar={this.props.expandSidebar}
|
||||||
|
collapseSidebar={this.props.collapseSidebar}
|
||||||
/>
|
/>
|
||||||
|
<div className="editor-console-container">
|
||||||
<Editor
|
<Editor
|
||||||
file={this.props.selectedFile}
|
file={this.props.selectedFile}
|
||||||
updateFileContent={this.props.updateFileContent}
|
updateFileContent={this.props.updateFileContent}
|
||||||
fontSize={this.props.preferences.fontSize}
|
fontSize={this.props.preferences.fontSize}
|
||||||
|
indentationAmount={this.props.preferences.indentationAmount}
|
||||||
|
isTabIndent={this.props.preferences.isTabIndent}
|
||||||
files={this.props.files}
|
files={this.props.files}
|
||||||
/>
|
/>
|
||||||
|
<Console
|
||||||
|
consoleEvent={this.props.ide.consoleEvent}
|
||||||
|
isPlaying={this.props.ide.isPlaying}
|
||||||
|
isExpanded={this.props.ide.consoleIsExpanded}
|
||||||
|
expandConsole={this.props.expandConsole}
|
||||||
|
collapseConsole={this.props.collapseConsole}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<PreviewFrame
|
<PreviewFrame
|
||||||
htmlFile={this.props.htmlFile}
|
htmlFile={this.props.htmlFile}
|
||||||
jsFiles={this.props.jsFiles}
|
jsFiles={this.props.jsFiles}
|
||||||
|
@ -67,8 +96,22 @@ class IDEView extends React.Component {
|
||||||
<link type="text/css" rel="stylesheet" href="/preview-styles.css" />
|
<link type="text/css" rel="stylesheet" href="/preview-styles.css" />
|
||||||
}
|
}
|
||||||
isPlaying={this.props.ide.isPlaying}
|
isPlaying={this.props.ide.isPlaying}
|
||||||
|
dispatchConsoleEvent={this.props.dispatchConsoleEvent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{(() => {
|
||||||
|
if (this.props.ide.modalIsVisible) {
|
||||||
|
return (
|
||||||
|
<NewFileModal
|
||||||
|
canUploadMedia={this.props.user.authenticated}
|
||||||
|
closeModal={this.props.closeNewFileModal}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,26 +121,43 @@ IDEView.propTypes = {
|
||||||
project_id: PropTypes.string
|
project_id: PropTypes.string
|
||||||
}),
|
}),
|
||||||
getProject: PropTypes.func.isRequired,
|
getProject: PropTypes.func.isRequired,
|
||||||
user: PropTypes.object.isRequired,
|
user: PropTypes.shape({
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
createProject: PropTypes.func.isRequired,
|
createProject: PropTypes.func.isRequired,
|
||||||
saveProject: PropTypes.func.isRequired,
|
saveProject: PropTypes.func.isRequired,
|
||||||
ide: PropTypes.shape({
|
ide: PropTypes.shape({
|
||||||
isPlaying: PropTypes.bool.isRequired
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
|
consoleEvent: PropTypes.object,
|
||||||
|
modalIsVisible: PropTypes.bool.isRequired,
|
||||||
|
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
consoleIsExpanded: PropTypes.bool.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
startSketch: PropTypes.func.isRequired,
|
startSketch: PropTypes.func.isRequired,
|
||||||
stopSketch: PropTypes.func.isRequired,
|
stopSketch: PropTypes.func.isRequired,
|
||||||
project: PropTypes.shape({
|
project: PropTypes.shape({
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
username: PropTypes.string
|
||||||
|
})
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
setProjectName: PropTypes.func.isRequired,
|
setProjectName: PropTypes.func.isRequired,
|
||||||
openPreferences: PropTypes.func.isRequired,
|
openPreferences: PropTypes.func.isRequired,
|
||||||
preferences: PropTypes.shape({
|
preferences: PropTypes.shape({
|
||||||
isVisible: PropTypes.bool.isRequired,
|
isVisible: PropTypes.bool.isRequired,
|
||||||
fontSize: PropTypes.number.isRequired
|
fontSize: PropTypes.number.isRequired,
|
||||||
|
indentationAmount: PropTypes.number.isRequired,
|
||||||
|
isTabIndent: PropTypes.bool.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
closePreferences: PropTypes.func.isRequired,
|
closePreferences: PropTypes.func.isRequired,
|
||||||
increaseFont: PropTypes.func.isRequired,
|
increaseFont: PropTypes.func.isRequired,
|
||||||
decreaseFont: PropTypes.func.isRequired,
|
decreaseFont: PropTypes.func.isRequired,
|
||||||
|
updateFont: PropTypes.func.isRequired,
|
||||||
|
increaseIndentation: PropTypes.func.isRequired,
|
||||||
|
decreaseIndentation: PropTypes.func.isRequired,
|
||||||
|
updateIndentation: PropTypes.func.isRequired,
|
||||||
|
indentWithSpace: PropTypes.func.isRequired,
|
||||||
|
indentWithTab: PropTypes.func.isRequired,
|
||||||
files: PropTypes.array.isRequired,
|
files: PropTypes.array.isRequired,
|
||||||
updateFileContent: PropTypes.func.isRequired,
|
updateFileContent: PropTypes.func.isRequired,
|
||||||
selectedFile: PropTypes.shape({
|
selectedFile: PropTypes.shape({
|
||||||
|
@ -107,7 +167,16 @@ IDEView.propTypes = {
|
||||||
setSelectedFile: PropTypes.func.isRequired,
|
setSelectedFile: PropTypes.func.isRequired,
|
||||||
htmlFile: PropTypes.object.isRequired,
|
htmlFile: PropTypes.object.isRequired,
|
||||||
jsFiles: PropTypes.array.isRequired,
|
jsFiles: PropTypes.array.isRequired,
|
||||||
cssFiles: PropTypes.array.isRequired
|
cssFiles: PropTypes.array.isRequired,
|
||||||
|
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||||
|
newFile: PropTypes.func.isRequired,
|
||||||
|
closeNewFileModal: PropTypes.func.isRequired,
|
||||||
|
expandSidebar: PropTypes.func.isRequired,
|
||||||
|
collapseSidebar: PropTypes.func.isRequired,
|
||||||
|
exportProjectAsZip: PropTypes.func.isRequired,
|
||||||
|
cloneProject: PropTypes.func.isRequired,
|
||||||
|
expandConsole: PropTypes.func.isRequired,
|
||||||
|
collapseConsole: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
|
|
@ -12,7 +12,9 @@ const defaultHTML =
|
||||||
`<!DOCTYPE html>
|
`<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.2/p5.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.2/addons/p5.dom.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.2/addons/p5.sound.min.js"></script>
|
||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -58,10 +60,19 @@ const files = (state = initialState, action) => {
|
||||||
|
|
||||||
return Object.assign({}, file, { content: action.content });
|
return Object.assign({}, file, { content: action.content });
|
||||||
});
|
});
|
||||||
|
case ActionTypes.SET_BLOB_URL:
|
||||||
|
return state.map(file => {
|
||||||
|
if (file.name !== action.name) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
return Object.assign({}, file, { blobURL: action.blobURL });
|
||||||
|
});
|
||||||
case ActionTypes.NEW_PROJECT:
|
case ActionTypes.NEW_PROJECT:
|
||||||
return [...action.files];
|
return [...action.files];
|
||||||
case ActionTypes.SET_PROJECT:
|
case ActionTypes.SET_PROJECT:
|
||||||
return [...action.files];
|
return [...action.files];
|
||||||
|
case ActionTypes.CREATE_FILE:
|
||||||
|
return [...state, { name: action.name, id: action.id, content: '', url: action.url }];
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -71,5 +82,6 @@ export const getFile = (state, id) => state.filter(file => file.id === id)[0];
|
||||||
export const getHTMLFile = (state) => state.filter(file => file.name.match(/.*\.html$/))[0];
|
export const getHTMLFile = (state) => state.filter(file => file.name.match(/.*\.html$/))[0];
|
||||||
export const getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/));
|
export const getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/));
|
||||||
export const getCSSFiles = (state) => state.filter(file => file.name.match(/.*\.css$/));
|
export const getCSSFiles = (state) => state.filter(file => file.name.match(/.*\.css$/));
|
||||||
|
export const getLinkedFiles = (state) => state.filter(file => file.url);
|
||||||
|
|
||||||
export default files;
|
export default files;
|
||||||
|
|
|
@ -2,7 +2,14 @@ import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
selectedFile: '1'
|
selectedFile: '1',
|
||||||
|
consoleEvent: {
|
||||||
|
method: undefined,
|
||||||
|
arguments: []
|
||||||
|
},
|
||||||
|
modalIsVisible: false,
|
||||||
|
sidebarIsExpanded: true,
|
||||||
|
consoleIsExpanded: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const ide = (state = initialState, action) => {
|
const ide = (state = initialState, action) => {
|
||||||
|
@ -17,6 +24,20 @@ const ide = (state = initialState, action) => {
|
||||||
case ActionTypes.SET_PROJECT:
|
case ActionTypes.SET_PROJECT:
|
||||||
case ActionTypes.NEW_PROJECT:
|
case ActionTypes.NEW_PROJECT:
|
||||||
return Object.assign({}, state, { selectedFile: action.selectedFile });
|
return Object.assign({}, state, { selectedFile: action.selectedFile });
|
||||||
|
case ActionTypes.CONSOLE_EVENT:
|
||||||
|
return Object.assign({}, state, { consoleEvent: action.event });
|
||||||
|
case ActionTypes.SHOW_MODAL:
|
||||||
|
return Object.assign({}, state, { modalIsVisible: true });
|
||||||
|
case ActionTypes.HIDE_MODAL:
|
||||||
|
return Object.assign({}, state, { modalIsVisible: false });
|
||||||
|
case ActionTypes.COLLAPSE_SIDEBAR:
|
||||||
|
return Object.assign({}, state, { sidebarIsExpanded: false });
|
||||||
|
case ActionTypes.EXPAND_SIDEBAR:
|
||||||
|
return Object.assign({}, state, { sidebarIsExpanded: true });
|
||||||
|
case ActionTypes.COLLAPSE_CONSOLE:
|
||||||
|
return Object.assign({}, state, { consoleIsExpanded: false });
|
||||||
|
case ActionTypes.EXPAND_CONSOLE:
|
||||||
|
return Object.assign({}, state, { consoleIsExpanded: true });
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,31 +2,53 @@ import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
fontSize: 18
|
fontSize: 18,
|
||||||
|
indentationAmount: 2,
|
||||||
|
isTabIndent: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const preferences = (state = initialState, action) => {
|
const preferences = (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.OPEN_PREFERENCES:
|
case ActionTypes.OPEN_PREFERENCES:
|
||||||
return {
|
return Object.assign({}, state, {
|
||||||
isVisible: true,
|
isVisible: true
|
||||||
fontSize: state.fontSize
|
});
|
||||||
};
|
|
||||||
case ActionTypes.CLOSE_PREFERENCES:
|
case ActionTypes.CLOSE_PREFERENCES:
|
||||||
return {
|
return Object.assign({}, state, {
|
||||||
isVisible: false,
|
isVisible: false
|
||||||
fontSize: state.fontSize
|
});
|
||||||
};
|
|
||||||
case ActionTypes.INCREASE_FONTSIZE:
|
case ActionTypes.INCREASE_FONTSIZE:
|
||||||
return {
|
return Object.assign({}, state, {
|
||||||
isVisible: state.isVisible,
|
|
||||||
fontSize: state.fontSize + 2
|
fontSize: state.fontSize + 2
|
||||||
};
|
});
|
||||||
case ActionTypes.DECREASE_FONTSIZE:
|
case ActionTypes.DECREASE_FONTSIZE:
|
||||||
return {
|
return Object.assign({}, state, {
|
||||||
isVisible: state.isVisible,
|
|
||||||
fontSize: state.fontSize - 2
|
fontSize: state.fontSize - 2
|
||||||
};
|
});
|
||||||
|
case ActionTypes.UPDATE_FONTSIZE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
fontSize: parseInt(action.value, 10)
|
||||||
|
});
|
||||||
|
case ActionTypes.INCREASE_INDENTATION:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
indentationAmount: state.indentationAmount + 2
|
||||||
|
});
|
||||||
|
case ActionTypes.DECREASE_INDENTATION:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
indentationAmount: state.indentationAmount - 2
|
||||||
|
});
|
||||||
|
case ActionTypes.UPDATE_INDENTATION:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
indentationAmount: parseInt(action.value, 10)
|
||||||
|
});
|
||||||
|
case ActionTypes.INDENT_WITH_TAB:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
isTabIndent: true
|
||||||
|
});
|
||||||
|
case ActionTypes.INDENT_WITH_SPACE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
isTabIndent: false
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,18 @@ const initialState = {
|
||||||
const project = (state = initialState, action) => {
|
const project = (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.SET_PROJECT_NAME:
|
case ActionTypes.SET_PROJECT_NAME:
|
||||||
return {
|
return Object.assign({}, { ...state }, { name: action.name });
|
||||||
name: action.name
|
|
||||||
};
|
|
||||||
case ActionTypes.NEW_PROJECT:
|
case ActionTypes.NEW_PROJECT:
|
||||||
return {
|
return {
|
||||||
id: action.id,
|
id: action.id,
|
||||||
name: action.name
|
name: action.name,
|
||||||
|
owner: action.owner
|
||||||
};
|
};
|
||||||
case ActionTypes.SET_PROJECT:
|
case ActionTypes.SET_PROJECT:
|
||||||
return {
|
return {
|
||||||
id: action.project.id,
|
id: action.project.id,
|
||||||
name: action.project.name
|
name: action.project.name,
|
||||||
|
owner: action.owner
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -20,16 +20,16 @@ class SketchListView extends React.Component {
|
||||||
createProject={this.props.createProject}
|
createProject={this.props.createProject}
|
||||||
saveProject={this.props.saveProject}
|
saveProject={this.props.saveProject}
|
||||||
/>
|
/>
|
||||||
<table className="sketches-table">
|
<table className="sketches-table" summary="table containing all saved projects">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Name</th>
|
<th scope="col">Name</th>
|
||||||
<th>Created</th>
|
<th scope="col">Created</th>
|
||||||
<th>Last Updated</th>
|
<th scope="col">Last Updated</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.props.sketches.map(sketch =>
|
{this.props.sketches.map(sketch =>
|
||||||
<tr className="sketches-table__row">
|
<tr className="sketches-table__row">
|
||||||
<td><Link to={`/projects/${sketch._id}`}>{sketch.name}</Link></td>
|
<td scope="row"><Link to={`/projects/${sketch._id}`}>{sketch.name}</Link></td>
|
||||||
<td>{moment(sketch.createdAt).format('MMM D, YYYY')}</td>
|
<td>{moment(sketch.createdAt).format('MMM D, YYYY')}</td>
|
||||||
<td>{moment(sketch.updatedAt).format('MMM D, YYYY')}</td>
|
<td>{moment(sketch.updatedAt).format('MMM D, YYYY')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $light-icon-hover-color;
|
color: $light-icon-hover-color;
|
||||||
& g {
|
& g {
|
||||||
|
opacity: 1;
|
||||||
fill: $light-icon-hover-color;
|
fill: $light-icon-hover-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,6 +70,8 @@
|
||||||
@extend %toolbar-button;
|
@extend %toolbar-button;
|
||||||
color: $light-primary-text-color;
|
color: $light-primary-text-color;
|
||||||
background-color: $light-modal-button-background-color;
|
background-color: $light-modal-button-background-color;
|
||||||
|
padding: 0;
|
||||||
|
line-height: #{50 / $base-font-size}rem;
|
||||||
& g {
|
& g {
|
||||||
fill: $light-primary-text-color;
|
fill: $light-primary-text-color;
|
||||||
}
|
}
|
||||||
|
@ -88,3 +91,35 @@
|
||||||
color: $light-primary-text-color;
|
color: $light-primary-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%preference-option {
|
||||||
|
background-color: $light-button-background-color;
|
||||||
|
color: $light-inactive-text-color;
|
||||||
|
font-size: #{14 / $base-font-size}rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: #{5 / $base-font-size}rem;
|
||||||
|
border: 0px;
|
||||||
|
&:hover {
|
||||||
|
color: $light-primary-text-color;
|
||||||
|
}
|
||||||
|
&--selected {
|
||||||
|
color: $light-primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
%hidden-label {
|
||||||
|
position:absolute;
|
||||||
|
left:-10000px;
|
||||||
|
top:auto;
|
||||||
|
width:1px;
|
||||||
|
height:1px;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
%modal {
|
||||||
|
background-color: $light-modal-background-color;
|
||||||
|
border: 1px solid $light-modal-border-color;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 12px 12px $light-shadow-color;
|
||||||
|
}
|
||||||
|
|
|
@ -18,9 +18,12 @@ $light-button-background-hover-color: $p5js-pink;
|
||||||
$light-button-background-active-color: #f10046;
|
$light-button-background-active-color: #f10046;
|
||||||
$light-button-hover-color: $white;
|
$light-button-hover-color: $white;
|
||||||
$light-button-active-color: $white;
|
$light-button-active-color: $white;
|
||||||
|
$light-modal-background-color: #f4f4f4;
|
||||||
$light-modal-button-background-color: #e6e6e6;
|
$light-modal-button-background-color: #e6e6e6;
|
||||||
|
$light-modal-border-color: #B9D0E1;
|
||||||
$light-icon-color: #8b8b8b;
|
$light-icon-color: #8b8b8b;
|
||||||
$light-icon-hover-color: $light-primary-text-color;
|
$light-icon-hover-color: $light-primary-text-color;
|
||||||
|
$light-shadow-color: rgba(0, 0, 0, 0.16);
|
||||||
|
|
||||||
$dark-primary-text-color: $white;
|
$dark-primary-text-color: $white;
|
||||||
$dark-secondary-text-color: #c2c2c2;
|
$dark-secondary-text-color: #c2c2c2;
|
||||||
|
@ -38,3 +41,10 @@ $dark-button-active-color: $white;
|
||||||
$ide-border-color: #f4f4f4;
|
$ide-border-color: #f4f4f4;
|
||||||
$editor-selected-line-color: #f3f3f3;
|
$editor-selected-line-color: #f3f3f3;
|
||||||
$input-border-color: #979797;
|
$input-border-color: #979797;
|
||||||
|
|
||||||
|
$console-light-background-color: #eee;
|
||||||
|
$console-header-background-color: #d6d6d6;
|
||||||
|
$console-header-color: #b1b1b1;
|
||||||
|
$console-warn-color: #ffbe05;
|
||||||
|
$console-error-color: #ff5f52;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ body, input, button {
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: $light-inactive-text-color;
|
color: $light-inactive-text-color;
|
||||||
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: $light-primary-text-color;
|
color: $light-primary-text-color;
|
||||||
|
@ -31,8 +32,9 @@ input, button {
|
||||||
|
|
||||||
input {
|
input {
|
||||||
padding: #{5 / $base-font-size}rem;
|
padding: #{5 / $base-font-size}rem;
|
||||||
border-radius: 2px;
|
// border-radius: 2px;
|
||||||
border: 1px solid $input-border-color;
|
border: 1px solid $input-border-color;
|
||||||
|
padding: #{10 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
|
@ -46,7 +48,12 @@ h2 {
|
||||||
h3 {
|
h3 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
h4 {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
thead {
|
thead {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
67
client/styles/components/_console.scss
Normal file
67
client/styles/components/_console.scss
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
.preview-console {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: #{150 / $base-font-size}rem;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: $console-light-background-color;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& > {
|
||||||
|
position:relative;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign styles to different types of console messages
|
||||||
|
.preview-console__log {
|
||||||
|
color: $dark-secondary-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-console__error {
|
||||||
|
color: $console-error-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-console__warn {
|
||||||
|
color: $console-warn-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.preview-console--collapsed {
|
||||||
|
height: #{29 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-console__header {
|
||||||
|
background-color: $console-header-background-color;
|
||||||
|
color: $console-header-color;
|
||||||
|
padding: #{5 / $base-font-size}rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-console__header-title {
|
||||||
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-console__messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: #{121 / $base-font-size}rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-console__collapse {
|
||||||
|
.preview-console--collapsed & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-console__expand {
|
||||||
|
display: none;
|
||||||
|
.preview-console--collapsed & {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,3 +19,44 @@
|
||||||
.CodeMirror-line {
|
.CodeMirror-line {
|
||||||
padding-left: #{5 / $base-font-size}rem;
|
padding-left: #{5 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CodeMirror-gutter-wrapper {
|
||||||
|
right: 100%;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-warning, .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-multiple {
|
||||||
|
background-image: none;
|
||||||
|
width: 70px;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
|
||||||
|
background-image: none;
|
||||||
|
padding-left: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-warning {
|
||||||
|
background-color: rgb(255, 190, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-error {
|
||||||
|
background-color: rgb(255, 95, 82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-gutter-elt:not(.CodeMirror-linenumber) {
|
||||||
|
opacity: 0.3;
|
||||||
|
width: 70px !important;
|
||||||
|
height: 100%;
|
||||||
|
// background-color: rgb(255, 95, 82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-tooltip {
|
||||||
|
background-color: $light-modal-background-color;
|
||||||
|
border: 1px solid $light-modal-border-color;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 12px 12px $light-shadow-color;
|
||||||
|
font-family: Montserrat, sans-serif;
|
||||||
|
}
|
||||||
|
|
45
client/styles/components/_modal.scss
Normal file
45
client/styles/components/_modal.scss
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
top: #{66 / $base-font-size}rem;
|
||||||
|
right: #{400 / $base-font-size}rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
@extend %modal;
|
||||||
|
height: #{400 / $base-font-size}rem;
|
||||||
|
width: #{400 / $base-font-size}rem;
|
||||||
|
padding: #{20 / $base-font-size}rem;
|
||||||
|
.modal--reduced & {
|
||||||
|
height: #{150 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__exit-button {
|
||||||
|
@extend %icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: #{20 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-file-form__name-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-file-form__name-input {
|
||||||
|
margin-right: #{10 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__divider {
|
||||||
|
text-align: center;
|
||||||
|
margin: #{20 / $base-font-size}rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader {
|
||||||
|
height: #{200 / $base-font-size}rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -27,6 +27,7 @@
|
||||||
.cm-s-p5-widget span.cm-error { color: #f00; }
|
.cm-s-p5-widget span.cm-error { color: #f00; }
|
||||||
|
|
||||||
.cm-s-p5-widget .CodeMirror-activeline-background { background-color: #e8f2ff; }
|
.cm-s-p5-widget .CodeMirror-activeline-background { background-color: #e8f2ff; }
|
||||||
|
// .cm-s-p5-widget .CodeMirror-activeline-gutter { background-color: #e8f2ff; }
|
||||||
.cm-s-p5-widget .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
|
.cm-s-p5-widget .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
|
||||||
|
|
||||||
/* These styles don't seem to be set by CodeMirror's javascript mode. */
|
/* These styles don't seem to be set by CodeMirror's javascript mode. */
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
.preferences {
|
.preferences {
|
||||||
|
@extend %modal;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: #{66 / $base-font-size}rem;
|
top: #{66 / $base-font-size}rem;
|
||||||
right: #{40 / $base-font-size}rem;
|
right: #{40 / $base-font-size}rem;
|
||||||
width: #{276 / $base-font-size}rem;
|
width: #{336 / $base-font-size}rem;
|
||||||
background-color: $light-button-background-color;
|
|
||||||
display: none;
|
display: none;
|
||||||
padding: #{16 / $base-font-size}rem #{26 / $base-font-size}rem;
|
padding: #{16 / $base-font-size}rem #{26 / $base-font-size}rem;
|
||||||
&--selected {
|
&--selected {
|
||||||
|
@ -35,7 +35,10 @@
|
||||||
.preference {
|
.preference {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
padding-bottom: #{40 / $base-font-size}rem;
|
||||||
|
& + & {
|
||||||
|
border-top: 2px dashed $light-button-border-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preference__title {
|
.preference__title {
|
||||||
|
@ -44,8 +47,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.preference__value {
|
.preference__value {
|
||||||
border: 1px solid $light-button-border-color;
|
border: 2px solid $light-button-border-color;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: #{48 / $base-font-size}rem;
|
border-radius: 0%;
|
||||||
width: #{48 / $base-font-size}rem;
|
width: #{48 / $base-font-size}rem;
|
||||||
|
height: #{44 / $base-font-size}rem;
|
||||||
|
margin: 0 #{28 / $base-font-size}rem;
|
||||||
|
padding: 0;
|
||||||
|
background-color: $light-button-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preference__label {
|
||||||
|
margin: 0;
|
||||||
|
line-height: #{20 / $base-font-size}rem;
|
||||||
|
color: $light-inactive-text-color;
|
||||||
|
&:hover {
|
||||||
|
color: $light-inactive-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preference__vertical-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preference__option {
|
||||||
|
@extend %preference-option;
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: #{28 / $base-font-size}rem;
|
||||||
|
&--selected {
|
||||||
|
@extend %preference-option--selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preference__button-label {
|
||||||
|
@extend %hidden-label
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,71 @@
|
||||||
|
.sidebar__header {
|
||||||
|
padding: #{10 / $base-font-size}rem #{6 / $base-font-size}rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid $ide-border-color;
|
||||||
|
align-items: center;
|
||||||
|
height: #{47 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__title {
|
||||||
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
display: inline-block;
|
||||||
|
.sidebar--contracted & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__add {
|
||||||
|
cursor: pointer;
|
||||||
|
height: #{26 / $base-font-size}rem;
|
||||||
|
margin-right: #{16 / $base-font-size}rem;
|
||||||
|
font-size: #{24 / $base-font-size}rem;
|
||||||
|
.sidebar--contracted & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar__file-list {
|
.sidebar__file-list {
|
||||||
border-top: 1px solid $ide-border-color;
|
border-top: 1px solid $ide-border-color;
|
||||||
|
.sidebar--contracted & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__file-item {
|
.sidebar__file-item {
|
||||||
|
font-size: #{16 / $base-font-size}rem;
|
||||||
padding: #{8 / $base-font-size}rem #{20 / $base-font-size}rem;
|
padding: #{8 / $base-font-size}rem #{20 / $base-font-size}rem;
|
||||||
color: $light-inactive-text-color;
|
color: $light-inactive-text-color;
|
||||||
}
|
cursor: pointer;
|
||||||
|
&--selected {
|
||||||
.sidebar__file-item--selected {
|
|
||||||
background-color: $ide-border-color;
|
background-color: $ide-border-color;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__contract {
|
||||||
|
@extend %icon;
|
||||||
|
height: #{14 / $base-font-size}rem;
|
||||||
|
& svg {
|
||||||
|
height: #{14 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
.sidebar--contracted & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__expand {
|
||||||
|
@extend %icon;
|
||||||
|
height: #{14 / $base-font-size}rem;
|
||||||
|
& svg {
|
||||||
|
height: #{14 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
display: none;
|
||||||
|
.sidebar--contracted & {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
|
@ -54,3 +54,10 @@
|
||||||
color: $light-inactive-text-color;
|
color: $light-inactive-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar__button-label {
|
||||||
|
@extend %hidden-label
|
||||||
|
}
|
||||||
|
.toolbar__project-owner {
|
||||||
|
margin-left: #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,30 @@
|
||||||
.ide {
|
.ide {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-preview-container {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 0 0px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-console-container {
|
||||||
|
flex: 1 0 0px;
|
||||||
|
max-width: 45%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-holder {
|
.editor-holder {
|
||||||
flex-grow: 1;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-frame {
|
.preview-frame {
|
||||||
flex-grow: 1;
|
flex: 1 0 0px;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
@ -19,5 +32,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: #{140 / $base-font-size}rem;
|
width: #{180 / $base-font-size}rem;
|
||||||
|
&.sidebar--contracted {
|
||||||
|
width: #{20 / $base-font-size}rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
@import 'base/base';
|
@import 'base/base';
|
||||||
|
|
||||||
@import 'vendors/codemirror';
|
@import 'vendors/codemirror';
|
||||||
|
@import 'vendors/lint';
|
||||||
|
@import 'vendors/dropzone';
|
||||||
|
|
||||||
@import 'components/p5-widget-codemirror-theme';
|
@import 'components/p5-widget-codemirror-theme';
|
||||||
@import 'components/editor';
|
@import 'components/editor';
|
||||||
|
@ -15,6 +17,8 @@
|
||||||
@import 'components/login';
|
@import 'components/login';
|
||||||
@import 'components/sketch-list';
|
@import 'components/sketch-list';
|
||||||
@import 'components/sidebar';
|
@import 'components/sidebar';
|
||||||
|
@import 'components/modal';
|
||||||
|
@import 'components/console';
|
||||||
|
|
||||||
@import 'layout/ide';
|
@import 'layout/ide';
|
||||||
@import 'layout/sketch-list';
|
@import 'layout/sketch-list';
|
||||||
|
|
388
client/styles/vendors/_dropzone.scss
vendored
Normal file
388
client/styles/vendors/_dropzone.scss
vendored
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright (c) 2012 Matias Meno <m@tias.me>
|
||||||
|
*/
|
||||||
|
@-webkit-keyframes passing-through {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(40px);
|
||||||
|
-moz-transform: translateY(40px);
|
||||||
|
-ms-transform: translateY(40px);
|
||||||
|
-o-transform: translateY(40px);
|
||||||
|
transform: translateY(40px); }
|
||||||
|
30%, 70% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-moz-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
-o-transform: translateY(0px);
|
||||||
|
transform: translateY(0px); }
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-40px);
|
||||||
|
-moz-transform: translateY(-40px);
|
||||||
|
-ms-transform: translateY(-40px);
|
||||||
|
-o-transform: translateY(-40px);
|
||||||
|
transform: translateY(-40px); } }
|
||||||
|
@-moz-keyframes passing-through {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(40px);
|
||||||
|
-moz-transform: translateY(40px);
|
||||||
|
-ms-transform: translateY(40px);
|
||||||
|
-o-transform: translateY(40px);
|
||||||
|
transform: translateY(40px); }
|
||||||
|
30%, 70% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-moz-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
-o-transform: translateY(0px);
|
||||||
|
transform: translateY(0px); }
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-40px);
|
||||||
|
-moz-transform: translateY(-40px);
|
||||||
|
-ms-transform: translateY(-40px);
|
||||||
|
-o-transform: translateY(-40px);
|
||||||
|
transform: translateY(-40px); } }
|
||||||
|
@keyframes passing-through {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(40px);
|
||||||
|
-moz-transform: translateY(40px);
|
||||||
|
-ms-transform: translateY(40px);
|
||||||
|
-o-transform: translateY(40px);
|
||||||
|
transform: translateY(40px); }
|
||||||
|
30%, 70% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-moz-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
-o-transform: translateY(0px);
|
||||||
|
transform: translateY(0px); }
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-40px);
|
||||||
|
-moz-transform: translateY(-40px);
|
||||||
|
-ms-transform: translateY(-40px);
|
||||||
|
-o-transform: translateY(-40px);
|
||||||
|
transform: translateY(-40px); } }
|
||||||
|
@-webkit-keyframes slide-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(40px);
|
||||||
|
-moz-transform: translateY(40px);
|
||||||
|
-ms-transform: translateY(40px);
|
||||||
|
-o-transform: translateY(40px);
|
||||||
|
transform: translateY(40px); }
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-moz-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
-o-transform: translateY(0px);
|
||||||
|
transform: translateY(0px); } }
|
||||||
|
@-moz-keyframes slide-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(40px);
|
||||||
|
-moz-transform: translateY(40px);
|
||||||
|
-ms-transform: translateY(40px);
|
||||||
|
-o-transform: translateY(40px);
|
||||||
|
transform: translateY(40px); }
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-moz-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
-o-transform: translateY(0px);
|
||||||
|
transform: translateY(0px); } }
|
||||||
|
@keyframes slide-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(40px);
|
||||||
|
-moz-transform: translateY(40px);
|
||||||
|
-ms-transform: translateY(40px);
|
||||||
|
-o-transform: translateY(40px);
|
||||||
|
transform: translateY(40px); }
|
||||||
|
30% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-moz-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
-o-transform: translateY(0px);
|
||||||
|
transform: translateY(0px); } }
|
||||||
|
@-webkit-keyframes pulse {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1); }
|
||||||
|
10% {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
-moz-transform: scale(1.1);
|
||||||
|
-ms-transform: scale(1.1);
|
||||||
|
-o-transform: scale(1.1);
|
||||||
|
transform: scale(1.1); }
|
||||||
|
20% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1); } }
|
||||||
|
@-moz-keyframes pulse {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1); }
|
||||||
|
10% {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
-moz-transform: scale(1.1);
|
||||||
|
-ms-transform: scale(1.1);
|
||||||
|
-o-transform: scale(1.1);
|
||||||
|
transform: scale(1.1); }
|
||||||
|
20% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1); } }
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1); }
|
||||||
|
10% {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
-moz-transform: scale(1.1);
|
||||||
|
-ms-transform: scale(1.1);
|
||||||
|
-o-transform: scale(1.1);
|
||||||
|
transform: scale(1.1); }
|
||||||
|
20% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1); } }
|
||||||
|
.dropzone, .dropzone * {
|
||||||
|
box-sizing: border-box; }
|
||||||
|
|
||||||
|
.dropzone {
|
||||||
|
min-height: 150px;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.3);
|
||||||
|
background: white;
|
||||||
|
padding: 20px 20px; }
|
||||||
|
.dropzone.dz-clickable {
|
||||||
|
cursor: pointer; }
|
||||||
|
.dropzone.dz-clickable * {
|
||||||
|
cursor: default; }
|
||||||
|
.dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * {
|
||||||
|
cursor: pointer; }
|
||||||
|
.dropzone.dz-started .dz-message {
|
||||||
|
display: none; }
|
||||||
|
.dropzone.dz-drag-hover {
|
||||||
|
border-style: solid; }
|
||||||
|
.dropzone.dz-drag-hover .dz-message {
|
||||||
|
opacity: 0.5; }
|
||||||
|
.dropzone .dz-message {
|
||||||
|
text-align: center;
|
||||||
|
margin: 2em 0; }
|
||||||
|
.dropzone .dz-preview {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin: 16px;
|
||||||
|
min-height: 100px; }
|
||||||
|
.dropzone .dz-preview:hover {
|
||||||
|
z-index: 1000; }
|
||||||
|
.dropzone .dz-preview:hover .dz-details {
|
||||||
|
opacity: 1; }
|
||||||
|
.dropzone .dz-preview.dz-file-preview .dz-image {
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #999;
|
||||||
|
background: linear-gradient(to bottom, #eee, #ddd); }
|
||||||
|
.dropzone .dz-preview.dz-file-preview .dz-details {
|
||||||
|
opacity: 1; }
|
||||||
|
.dropzone .dz-preview.dz-image-preview {
|
||||||
|
background: white; }
|
||||||
|
.dropzone .dz-preview.dz-image-preview .dz-details {
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
-ms-transition: opacity 0.2s linear;
|
||||||
|
-o-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear; }
|
||||||
|
.dropzone .dz-preview .dz-remove {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none; }
|
||||||
|
.dropzone .dz-preview .dz-remove:hover {
|
||||||
|
text-decoration: underline; }
|
||||||
|
.dropzone .dz-preview:hover .dz-details {
|
||||||
|
opacity: 1; }
|
||||||
|
.dropzone .dz-preview .dz-details {
|
||||||
|
z-index: 20;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 2em 1em;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
line-height: 150%; }
|
||||||
|
.dropzone .dz-preview .dz-details .dz-size {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
font-size: 16px; }
|
||||||
|
.dropzone .dz-preview .dz-details .dz-filename {
|
||||||
|
white-space: nowrap; }
|
||||||
|
.dropzone .dz-preview .dz-details .dz-filename:hover span {
|
||||||
|
border: 1px solid rgba(200, 200, 200, 0.8);
|
||||||
|
background-color: rgba(255, 255, 255, 0.8); }
|
||||||
|
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis; }
|
||||||
|
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
|
||||||
|
border: 1px solid transparent; }
|
||||||
|
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
|
||||||
|
background-color: rgba(255, 255, 255, 0.4);
|
||||||
|
padding: 0 0.4em;
|
||||||
|
border-radius: 3px; }
|
||||||
|
.dropzone .dz-preview:hover .dz-image img {
|
||||||
|
-webkit-transform: scale(1.05, 1.05);
|
||||||
|
-moz-transform: scale(1.05, 1.05);
|
||||||
|
-ms-transform: scale(1.05, 1.05);
|
||||||
|
-o-transform: scale(1.05, 1.05);
|
||||||
|
transform: scale(1.05, 1.05);
|
||||||
|
-webkit-filter: blur(8px);
|
||||||
|
filter: blur(8px); }
|
||||||
|
.dropzone .dz-preview .dz-image {
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
z-index: 10; }
|
||||||
|
.dropzone .dz-preview .dz-image img {
|
||||||
|
display: block; }
|
||||||
|
.dropzone .dz-preview.dz-success .dz-success-mark {
|
||||||
|
-webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
-moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
-ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
-o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||||
|
.dropzone .dz-preview.dz-error .dz-error-mark {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
-moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
-ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
-o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); }
|
||||||
|
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 500;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -27px;
|
||||||
|
margin-top: -27px; }
|
||||||
|
.dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg {
|
||||||
|
display: block;
|
||||||
|
width: 54px;
|
||||||
|
height: 54px; }
|
||||||
|
.dropzone .dz-preview.dz-processing .dz-progress {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transition: all 0.2s linear;
|
||||||
|
-moz-transition: all 0.2s linear;
|
||||||
|
-ms-transition: all 0.2s linear;
|
||||||
|
-o-transition: all 0.2s linear;
|
||||||
|
transition: all 0.2s linear; }
|
||||||
|
.dropzone .dz-preview.dz-complete .dz-progress {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.4s ease-in;
|
||||||
|
-moz-transition: opacity 0.4s ease-in;
|
||||||
|
-ms-transition: opacity 0.4s ease-in;
|
||||||
|
-o-transition: opacity 0.4s ease-in;
|
||||||
|
transition: opacity 0.4s ease-in; }
|
||||||
|
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
|
||||||
|
-webkit-animation: pulse 6s ease infinite;
|
||||||
|
-moz-animation: pulse 6s ease infinite;
|
||||||
|
-ms-animation: pulse 6s ease infinite;
|
||||||
|
-o-animation: pulse 6s ease infinite;
|
||||||
|
animation: pulse 6s ease infinite; }
|
||||||
|
.dropzone .dz-preview .dz-progress {
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
height: 16px;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -8px;
|
||||||
|
width: 80px;
|
||||||
|
margin-left: -40px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden; }
|
||||||
|
.dropzone .dz-preview .dz-progress .dz-upload {
|
||||||
|
background: #333;
|
||||||
|
background: linear-gradient(to bottom, #666, #444);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 0;
|
||||||
|
-webkit-transition: width 300ms ease-in-out;
|
||||||
|
-moz-transition: width 300ms ease-in-out;
|
||||||
|
-ms-transition: width 300ms ease-in-out;
|
||||||
|
-o-transition: width 300ms ease-in-out;
|
||||||
|
transition: width 300ms ease-in-out; }
|
||||||
|
.dropzone .dz-preview.dz-error .dz-error-message {
|
||||||
|
display: block; }
|
||||||
|
.dropzone .dz-preview.dz-error:hover .dz-error-message {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto; }
|
||||||
|
.dropzone .dz-preview .dz-error-message {
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.3s ease;
|
||||||
|
-moz-transition: opacity 0.3s ease;
|
||||||
|
-ms-transition: opacity 0.3s ease;
|
||||||
|
-o-transition: opacity 0.3s ease;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
top: 130px;
|
||||||
|
left: -10px;
|
||||||
|
width: 140px;
|
||||||
|
background: #be2626;
|
||||||
|
background: linear-gradient(to bottom, #be2626, #a92222);
|
||||||
|
padding: 0.5em 1.2em;
|
||||||
|
color: white; }
|
||||||
|
.dropzone .dz-preview .dz-error-message:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
left: 64px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-bottom: 6px solid #be2626; }
|
73
client/styles/vendors/_lint.scss
vendored
Normal file
73
client/styles/vendors/_lint.scss
vendored
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/* The lint marker gutter */
|
||||||
|
.CodeMirror-lint-markers {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-tooltip {
|
||||||
|
background-color: infobackground;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
color: infotext;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10pt;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2px 5px;
|
||||||
|
position: fixed;
|
||||||
|
white-space: pre;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
z-index: 100;
|
||||||
|
max-width: 600px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .4s;
|
||||||
|
-moz-transition: opacity .4s;
|
||||||
|
-webkit-transition: opacity .4s;
|
||||||
|
-o-transition: opacity .4s;
|
||||||
|
-ms-transition: opacity .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
|
||||||
|
background-position: left bottom;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-error {
|
||||||
|
background-image:
|
||||||
|
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-mark-warning {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
|
||||||
|
padding-left: 18px;
|
||||||
|
background-position: top left;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-lint-marker-multiple {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right bottom;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
}
|
18
package.json
18
package.json
|
@ -24,8 +24,10 @@
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.2.6",
|
"babel-plugin-transform-react-remove-prop-types": "^0.2.6",
|
||||||
"babel-polyfill": "^6.8.0",
|
"babel-polyfill": "^6.8.0",
|
||||||
"babel-preset-es2015": "^6.6.0",
|
"babel-preset-es2015": "^6.6.0",
|
||||||
|
"babel-preset-es2015-native-modules": "^6.9.2",
|
||||||
"babel-preset-react": "^6.5.0",
|
"babel-preset-react": "^6.5.0",
|
||||||
"babel-preset-react-hmre": "^1.1.1",
|
"babel-preset-react-hmre": "^1.1.1",
|
||||||
|
"babel-preset-react-optimize": "^1.0.1",
|
||||||
"babel-preset-stage-0": "^6.5.0",
|
"babel-preset-stage-0": "^6.5.0",
|
||||||
"babel-register": "^6.8.0",
|
"babel-register": "^6.8.0",
|
||||||
"css-loader": "^0.23.1",
|
"css-loader": "^0.23.1",
|
||||||
|
@ -56,22 +58,33 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"async": "^2.0.0",
|
||||||
"axios": "^0.12.0",
|
"axios": "^0.12.0",
|
||||||
"babel-core": "^6.8.0",
|
"babel-core": "^6.8.0",
|
||||||
"bcrypt-nodejs": "0.0.3",
|
"bcrypt-nodejs": "0.0.3",
|
||||||
|
"blob-util": "^1.2.1",
|
||||||
"body-parser": "^1.15.1",
|
"body-parser": "^1.15.1",
|
||||||
"bson-objectid": "^1.1.4",
|
"bson-objectid": "^1.1.4",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"codemirror": "^5.14.2",
|
"codemirror": "^5.14.2",
|
||||||
"connect-mongo": "^1.2.0",
|
"connect-mongo": "^1.2.0",
|
||||||
"cookie-parser": "^1.4.1",
|
"cookie-parser": "^1.4.1",
|
||||||
|
"csslint": "^0.10.0",
|
||||||
"dotenv": "^2.0.0",
|
"dotenv": "^2.0.0",
|
||||||
|
"dropzone": "^4.3.0",
|
||||||
"escape-string-regexp": "^1.0.5",
|
"escape-string-regexp": "^1.0.5",
|
||||||
"eslint-loader": "^1.3.0",
|
"eslint-loader": "^1.3.0",
|
||||||
"express": "^4.13.4",
|
"express": "^4.13.4",
|
||||||
"express-session": "^1.13.0",
|
"express-session": "^1.13.0",
|
||||||
|
"file-saver": "^1.3.2",
|
||||||
|
"file-type": "^3.8.0",
|
||||||
|
"htmlhint": "^0.9.13",
|
||||||
|
"jshint": "^2.9.2",
|
||||||
|
"jszip": "^3.0.0",
|
||||||
|
"jszip-utils": "0.0.2",
|
||||||
"moment": "^2.14.1",
|
"moment": "^2.14.1",
|
||||||
"mongoose": "^4.4.16",
|
"mongoose": "^4.4.16",
|
||||||
|
"node-uuid": "^1.4.7",
|
||||||
"passport": "^0.3.2",
|
"passport": "^0.3.2",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
@ -83,7 +96,10 @@
|
||||||
"redux": "^3.5.2",
|
"redux": "^3.5.2",
|
||||||
"redux-form": "^5.2.5",
|
"redux-form": "^5.2.5",
|
||||||
"redux-thunk": "^2.1.0",
|
"redux-thunk": "^2.1.0",
|
||||||
|
"s3-policy": "^0.2.0",
|
||||||
"shortid": "^2.2.6",
|
"shortid": "^2.2.6",
|
||||||
"srcdoc-polyfill": "^0.2.0"
|
"srcdoc-polyfill": "^0.2.0",
|
||||||
|
"throttle-debounce": "^1.0.1",
|
||||||
|
"xhr": "^2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
28
server/controllers/aws.controller.js
Normal file
28
server/controllers/aws.controller.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import uuid from 'node-uuid';
|
||||||
|
import policy from 's3-policy';
|
||||||
|
|
||||||
|
function getExtension(filename) {
|
||||||
|
const i = filename.lastIndexOf('.');
|
||||||
|
return (i < 0) ? '' : filename.substr(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signS3(req, res) {
|
||||||
|
const fileExtension = getExtension(req.body.name),
|
||||||
|
filename = uuid.v4() + fileExtension,
|
||||||
|
acl = 'public-read',
|
||||||
|
p = policy({
|
||||||
|
acl: acl,
|
||||||
|
secret: process.env.AWS_SECRET_KEY,
|
||||||
|
length: 5000000, // in bytes?
|
||||||
|
bucket: process.env.S3_BUCKET,
|
||||||
|
key: filename,
|
||||||
|
expires: new Date(Date.now() + 60000),
|
||||||
|
}),
|
||||||
|
result = {
|
||||||
|
'AWSAccessKeyId': process.env.AWS_ACCESS_KEY,
|
||||||
|
'key': filename,
|
||||||
|
'policy': p.policy,
|
||||||
|
'signature': p.signature
|
||||||
|
};
|
||||||
|
return res.json(result);
|
||||||
|
};
|
19
server/controllers/file.controller.js
Normal file
19
server/controllers/file.controller.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Project from '../models/project'
|
||||||
|
|
||||||
|
// Bug -> timestamps don't get created, but it seems like this will
|
||||||
|
// be fixed in mongoose soon
|
||||||
|
// https://github.com/Automattic/mongoose/issues/4049
|
||||||
|
export function createFile(req, res) {
|
||||||
|
Project.findByIdAndUpdate(req.params.project_id,
|
||||||
|
{
|
||||||
|
$push: {
|
||||||
|
'files': req.body
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}, (err, updatedProject) => {
|
||||||
|
if (err) { return res.json({ success: false }); }
|
||||||
|
return res.json(updatedProject.files[updatedProject.files.length - 1]);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,15 +1,20 @@
|
||||||
import Project from '../models/project';
|
import Project from '../models/project';
|
||||||
|
|
||||||
export function createProject(req, res) {
|
export function createProject(req, res) {
|
||||||
const projectValues = {
|
let projectValues = {
|
||||||
user: req.user ? req.user._id : undefined // eslint-disable-line no-underscore-dangle
|
user: req.user ? req.user._id : undefined // eslint-disable-line no-underscore-dangle
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(projectValues, req.body);
|
projectValues = Object.assign(projectValues, req.body);
|
||||||
|
|
||||||
Project.create(projectValues, (err, newProject) => {
|
Project.create(projectValues, (err, newProject) => {
|
||||||
if (err) { return res.json({ success: false }); }
|
if (err) { return res.json({ success: false }); }
|
||||||
return res.json(newProject);
|
Project.populate(newProject,
|
||||||
|
{path: 'user', select: 'username'},
|
||||||
|
(innerErr, newProjectWithUser) => {
|
||||||
|
if (innerErr) { return res.json({ success: false }); }
|
||||||
|
return res.json(newProjectWithUser);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,14 +22,18 @@ export function updateProject(req, res) {
|
||||||
Project.findByIdAndUpdate(req.params.project_id,
|
Project.findByIdAndUpdate(req.params.project_id,
|
||||||
{
|
{
|
||||||
$set: req.body
|
$set: req.body
|
||||||
}, (err, updatedProject) => {
|
})
|
||||||
|
.populate('user', 'username')
|
||||||
|
.exec((err, updatedProject) => {
|
||||||
if (err) { return res.json({ success: false }); }
|
if (err) { return res.json({ success: false }); }
|
||||||
return res.json(updatedProject);
|
return res.json(updatedProject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProject(req, res) {
|
export function getProject(req, res) {
|
||||||
Project.findById(req.params.project_id, (err, project) => {
|
Project.findById(req.params.project_id)
|
||||||
|
.populate('user', 'username')
|
||||||
|
.exec((err, project) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.status(404).send({ message: 'Project with that id does not exist' });
|
return res.status(404).send({ message: 'Project with that id does not exist' });
|
||||||
}
|
}
|
||||||
|
@ -37,7 +46,7 @@ export function getProjects(req, res) {
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
Project.find({user: req.user._id}) // eslint-disable-line no-underscore-dangle
|
Project.find({user: req.user._id}) // eslint-disable-line no-underscore-dangle
|
||||||
.sort('-createdAt')
|
.sort('-createdAt')
|
||||||
.select('name file _id createdAt updatedAt')
|
.select('name files _id createdAt updatedAt')
|
||||||
.exec((err, projects) => {
|
.exec((err, projects) => {
|
||||||
res.json(projects);
|
res.json(projects);
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,8 @@ const defaultCSS =
|
||||||
|
|
||||||
const fileSchema = new Schema({
|
const fileSchema = new Schema({
|
||||||
name: { type: String, default: 'sketch.js' },
|
name: { type: String, default: 'sketch.js' },
|
||||||
content: { type: String, default: defaultSketch }
|
content: { type: String },
|
||||||
|
url: { type: String }
|
||||||
}, { timestamps: true, _id: true });
|
}, { timestamps: true, _id: true });
|
||||||
|
|
||||||
fileSchema.virtual('id').get(function(){
|
fileSchema.virtual('id').get(function(){
|
||||||
|
|
8
server/routes/aws.routes.js
Normal file
8
server/routes/aws.routes.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import * as AWSController from '../controllers/aws.controller';
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.route('/S3/sign').post(AWSController.signS3);
|
||||||
|
|
||||||
|
export default router;
|
8
server/routes/file.routes.js
Normal file
8
server/routes/file.routes.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import * as FileController from '../controllers/file.controller';
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
router.route('/projects/:project_id/files').post(FileController.createFile);
|
||||||
|
|
||||||
|
export default router;
|
|
@ -27,6 +27,8 @@ import serverConfig from './config';
|
||||||
import users from './routes/user.routes';
|
import users from './routes/user.routes';
|
||||||
import sessions from './routes/session.routes';
|
import sessions from './routes/session.routes';
|
||||||
import projects from './routes/project.routes';
|
import projects from './routes/project.routes';
|
||||||
|
import files from './routes/file.routes';
|
||||||
|
import aws from './routes/aws.routes';
|
||||||
import serverRoutes from './routes/server.routes';
|
import serverRoutes from './routes/server.routes';
|
||||||
|
|
||||||
// Body parser, cookie parser, sessions, serve public assets
|
// Body parser, cookie parser, sessions, serve public assets
|
||||||
|
@ -55,6 +57,8 @@ app.use(passport.session());
|
||||||
app.use('/api', users);
|
app.use('/api', users);
|
||||||
app.use('/api', sessions);
|
app.use('/api', sessions);
|
||||||
app.use('/api', projects);
|
app.use('/api', projects);
|
||||||
|
app.use('/api', files);
|
||||||
|
app.use('/api', aws);
|
||||||
// this is supposed to be TEMPORARY -- until i figure out
|
// this is supposed to be TEMPORARY -- until i figure out
|
||||||
// isomorphic rendering
|
// isomorphic rendering
|
||||||
app.use('/', serverRoutes);
|
app.use('/', serverRoutes);
|
||||||
|
@ -83,4 +87,3 @@ app.listen(serverConfig.port, (error) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
var webpack = require('webpack');
|
var webpack = require('webpack');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devtool: 'cheap-module-eval-source-map',
|
devtool: 'cheap-module-eval-source-map',
|
||||||
entry: ['webpack-hot-middleware/client',
|
entry: ['babel-polyfill', 'webpack-hot-middleware/client',
|
||||||
'./client/index.js',
|
'./client/index.js',
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
|
@ -19,6 +20,7 @@ module.exports = {
|
||||||
'process.env': {
|
'process.env': {
|
||||||
CLIENT: JSON.stringify(true),
|
CLIENT: JSON.stringify(true),
|
||||||
'NODE_ENV': JSON.stringify('development'),
|
'NODE_ENV': JSON.stringify('development'),
|
||||||
|
'S3_BUCKET': '"' + process.env.S3_BUCKET + '"'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
@ -27,7 +29,7 @@ module.exports = {
|
||||||
{
|
{
|
||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
exclude: [/node_modules/, /.+\.config.js/],
|
exclude: [/node_modules/, /.+\.config.js/],
|
||||||
loaders: ['babel?presets[]=react-hmre', 'eslint-loader']
|
loaders: ['babel', 'eslint-loader']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
|
|
|
@ -4,11 +4,13 @@ var cssnext = require('postcss-cssnext');
|
||||||
var postcssFocus = require('postcss-focus');
|
var postcssFocus = require('postcss-focus');
|
||||||
var postcssReporter = require('postcss-reporter');
|
var postcssReporter = require('postcss-reporter');
|
||||||
var cssnano = require('cssnano');
|
var cssnano = require('cssnano');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devtool: 'hidden-source-map',
|
devtool: 'hidden-source-map',
|
||||||
|
|
||||||
entry: [
|
entry: [
|
||||||
|
'babel-polyfill',
|
||||||
'./client/index.js'
|
'./client/index.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -44,7 +46,8 @@ module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': {
|
'process.env': {
|
||||||
'NODE_ENV': JSON.stringify('production')
|
'NODE_ENV': JSON.stringify('production'),
|
||||||
|
'S3_BUCKET': '"' + process.env.S3_BUCKET + '"'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
|
Loading…
Reference in a new issue