Merge branch 'master' into login-signup-issue

This commit is contained in:
Cassie Tarakajian 2020-04-16 17:08:22 -04:00
commit e83ac7b587
34 changed files with 1802 additions and 678 deletions

View File

@ -33,6 +33,8 @@ Don't know where to begin? Here are some suggestions to get started:
- Front end: React/Redux, CSS/Sass, CodeMirror
- Back end: Node, Express, MongoDB, Jest, AWS
- DevOps: Travis CI, Jest, Docker, Kubernetes, AWS
- Documentation
- Translations: Application and documentation
* Use the [p5.js Web Editor](https://editor.p5js.org)! Find a bug? Think of something you think would add to the project? Open an issue.
* Expand an existing issue. Sometimes issues are missing steps to reproduce, or need suggestions for potential solutions. Sometimes they need another voice saying, "this is really important!"
* Try getting the project running locally on your computer by following the [installation steps](./../developer_docs/installation.md).

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: https://processingfoundation.org/support

View File

@ -1,5 +1,12 @@
# [p5.js Web Editor](https://editor.p5js.org)
Documentation is also available in the following languages:
[한국어](https://github.com/processing/p5.js-web-editor/blob/master/translations/ko)
## Welcome! 👋👋🏿👋🏽👋🏻👋🏾👋🏼
The p5.js Web Editor is a platform for creative coding, with a focus on making coding accessible for as many people as possible, including artists, designers, educators, beginners, and anyone who wants to learn. Simply by opening the website you can get started writing p5.js sketches without downloading or configuring anything. The editor is designed with simplicity in mind by limiting features and frills. We strive to listen to the community to drive the editors development, and to be intentional with every change. The editor is free and open-source.
We also strive to give the community as much ownership and control as possible. You can download your sketches so that you can edit them locally or host them elsewhere. You can also host your own version of the editor, giving you control over its data.

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { Link, browserHistory } from 'react-router';
import { Link } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import * as IDEActions from '../modules/IDE/actions/ide';
@ -167,8 +167,6 @@ class Nav extends React.PureComponent {
handleLogout() {
this.props.logoutUser();
// if you're on the settings page, probably.
browserHistory.push('/');
this.setDropdown('none');
}
@ -184,7 +182,8 @@ class Nav extends React.PureComponent {
}
handleShare() {
this.props.showShareModal();
const { username } = this.props.params;
this.props.showShareModal(this.props.project.id, this.props.project.name, username);
this.setDropdown('none');
}
@ -228,7 +227,7 @@ class Nav extends React.PureComponent {
renderDashboardMenu(navDropdownState) {
return (
<ul className="nav__items-left" title="project-menu">
<ul className="nav__items-left">
<li className="nav__item-logo">
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
</li>
@ -246,7 +245,7 @@ class Nav extends React.PureComponent {
renderProjectMenu(navDropdownState) {
return (
<ul className="nav__items-left" title="project-menu">
<ul className="nav__items-left">
<li className="nav__item-logo">
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
</li>
@ -717,6 +716,7 @@ Nav.propTypes = {
}).isRequired,
project: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
owner: PropTypes.shape({
id: PropTypes.string
})
@ -742,7 +742,10 @@ Nav.propTypes = {
layout: PropTypes.oneOf(['dashboard', 'project']),
rootFile: PropTypes.shape({
id: PropTypes.string.isRequired
}).isRequired
}).isRequired,
params: PropTypes.shape({
username: PropTypes.string
})
};
Nav.defaultProps = {
@ -752,7 +755,10 @@ Nav.defaultProps = {
},
cmController: {},
layout: 'project',
warnIfUnsavedChanges: undefined
warnIfUnsavedChanges: undefined,
params: {
username: undefined
}
};
function mapStateToProps(state) {

View File

@ -13,7 +13,7 @@ class NavBasic extends React.PureComponent {
render() {
return (
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
<ul className="nav__items-left" title="project-menu">
<ul className="nav__items-left">
<li className="nav__item-logo">
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
</li>

View File

@ -0,0 +1,71 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FileNode } from '../../modules/IDE/components/FileNode';
beforeAll(() => {});
describe('<FileNode />', () => {
let component;
let props = {};
describe('with valid props', () => {
beforeEach(() => {
props = {
...props,
id: '0',
children: [],
name: 'test.jsx',
fileType: 'dunno',
setSelectedFile: jest.fn(),
deleteFile: jest.fn(),
updateFileName: jest.fn(),
resetSelectedFile: jest.fn(),
newFile: jest.fn(),
newFolder: jest.fn(),
showFolderChildren: jest.fn(),
hideFolderChildren: jest.fn(),
canEdit: true,
authenticated: false
};
component = shallow(<FileNode {...props} />);
});
describe('when changing name', () => {
let input;
let renameTriggerButton;
const changeName = (newFileName) => {
renameTriggerButton.simulate('click');
input.simulate('change', { target: { value: newFileName } });
input.simulate('blur');
};
beforeEach(() => {
input = component.find('.sidebar__file-item-input');
renameTriggerButton = component
.find('.sidebar__file-item-option')
.first();
component.setState({ isEditing: true });
});
it('should render', () => expect(component).toBeDefined());
// it('should debug', () => console.log(component.debug()));
describe('to a valid filename', () => {
const newName = 'newname.jsx';
beforeEach(() => changeName(newName));
it('should save the name', () => {
expect(props.updateFileName).toBeCalledWith(props.id, newName);
});
});
describe('to an empty filename', () => {
const newName = '';
beforeEach(() => changeName(newName));
it('should not save the name', () => {
expect(props.updateFileName).not.toHaveBeenCalled();
});
});
});
});
});

View File

@ -7,7 +7,6 @@ exports[`Nav renders correctly 1`] = `
>
<ul
className="nav__items-left"
title="project-menu"
>
<li
className="nav__item-logo"

View File

@ -1,4 +1,5 @@
import axios from 'axios';
import { browserHistory } from 'react-router';
import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader';
import { setToastText, showToast } from './toast';
@ -47,20 +48,22 @@ export function createCollection(collection) {
});
dispatch(stopLoader());
const collectionName = response.data.name;
dispatch(setToastText(`Created "${collectionName}"`));
const newCollection = response.data;
dispatch(setToastText(`Created "${newCollection.name}"`));
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
return response.data;
const pathname = `/${newCollection.owner.username}/collections/${newCollection.id}`;
const location = { pathname, state: { skipSavingPath: true } };
browserHistory.push(location);
})
.catch((response) => {
console.error('Error creating collection', response.data);
dispatch({
type: ActionTypes.ERROR,
error: response.data
});
dispatch(stopLoader());
return response.data;
});
};
}

View File

@ -136,10 +136,13 @@ export function createFolder(formProps) {
}
export function updateFileName(id, name) {
return {
type: ActionTypes.UPDATE_FILE_NAME,
id,
name
return (dispatch) => {
dispatch(setUnsavedChanges(true));
dispatch({
type: ActionTypes.UPDATE_FILE_NAME,
id,
name
});
};
}

View File

@ -133,6 +133,7 @@ export function saveProject(selectedFile = null, autosave = false) {
}
const formParams = Object.assign({}, state.project);
formParams.files = [...state.files];
if (selectedFile) {
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
fileToUpdate.content = selectedFile.content;

View File

@ -6,39 +6,29 @@ import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files';
const downArrowUrl = require('../../../images/down-filled-triangle.svg');
const folderRightUrl = require('../../../images/triangle-arrow-right.svg');
const folderDownUrl = require('../../../images/triangle-arrow-down.svg');
const fileUrl = require('../../../images/file.svg');
import downArrowUrl from '../../../images/down-filled-triangle.svg';
import folderRightUrl from '../../../images/triangle-arrow-right.svg';
import folderDownUrl from '../../../images/triangle-arrow-down.svg';
import fileUrl from '../../../images/file.svg';
export class FileNode extends React.Component {
constructor(props) {
super(props);
this.renderChild = this.renderChild.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleFileNameChange = this.handleFileNameChange.bind(this);
this.validateFileName = this.validateFileName.bind(this);
this.handleFileClick = this.handleFileClick.bind(this);
this.toggleFileOptions = this.toggleFileOptions.bind(this);
this.hideFileOptions = this.hideFileOptions.bind(this);
this.showEditFileName = this.showEditFileName.bind(this);
this.hideEditFileName = this.hideEditFileName.bind(this);
this.onBlurComponent = this.onBlurComponent.bind(this);
this.onFocusComponent = this.onFocusComponent.bind(this);
this.state = {
isOptionsOpen: false,
isEditingName: false,
isFocused: false,
isDeleting: false,
updatedName: this.props.name
};
}
onFocusComponent() {
onFocusComponent = () => {
this.setState({ isFocused: true });
}
onBlurComponent() {
onBlurComponent = () => {
this.setState({ isFocused: false });
setTimeout(() => {
if (!this.state.isFocused) {
@ -47,41 +37,96 @@ export class FileNode extends React.Component {
}, 200);
}
handleFileClick(e) {
e.stopPropagation();
if (this.props.name !== 'root' && !this.isDeleting) {
this.props.setSelectedFile(this.props.id);
setUpdatedName = (updatedName) => {
this.setState({ updatedName });
}
saveUpdatedFileName = () => {
const { updatedName } = this.state;
const { name, updateFileName, id } = this.props;
if (updatedName !== name) {
updateFileName(id, updatedName);
}
}
handleFileNameChange(event) {
this.props.updateFileName(this.props.id, event.target.value);
handleFileClick = (event) => {
event.stopPropagation();
const { isDeleting } = this.state;
const { id, setSelectedFile, name } = this.props;
if (name !== 'root' && !isDeleting) {
setSelectedFile(id);
}
}
handleKeyPress(event) {
handleFileNameChange = (event) => {
const newName = event.target.value;
this.setUpdatedName(newName);
}
handleFileNameBlur = () => {
this.validateFileName();
this.hideEditFileName();
}
handleClickRename = () => {
this.setUpdatedName(this.props.name);
this.showEditFileName();
setTimeout(() => this.fileNameInput.focus(), 0);
setTimeout(() => this.hideFileOptions(), 0);
}
handleClickAddFile = () => {
this.props.newFile(this.props.id);
setTimeout(() => this.hideFileOptions(), 0);
}
handleClickAddFolder = () => {
this.props.newFolder(this.props.id);
setTimeout(() => this.hideFileOptions(), 0);
}
handleClickUploadFile = () => {
this.props.openUploadFileModal(this.props.id);
setTimeout(this.hideFileOptions, 0);
}
handleClickDelete = () => {
if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) {
this.setState({ isDeleting: true });
this.props.resetSelectedFile(this.props.id);
setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100);
}
}
handleKeyPress = (event) => {
if (event.key === 'Enter') {
this.hideEditFileName();
}
}
validateFileName() {
const oldFileExtension = this.originalFileName.match(/\.[0-9a-z]+$/i);
const newFileExtension = this.props.name.match(/\.[0-9a-z]+$/i);
const hasPeriod = this.props.name.match(/\.+/);
const newFileName = this.props.name;
validateFileName = () => {
const currentName = this.props.name;
const { updatedName } = this.state;
const oldFileExtension = currentName.match(/\.[0-9a-z]+$/i);
const newFileExtension = updatedName.match(/\.[0-9a-z]+$/i);
const hasPeriod = updatedName.match(/\.+/);
const hasNoExtension = oldFileExtension && !newFileExtension;
const hasExtensionIfFolder = this.props.fileType === 'folder' && hasPeriod;
const notSameExtension = oldFileExtension && newFileExtension
&& oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase();
const hasEmptyFilename = newFileName === '';
const hasOnlyExtension = newFileExtension && newFileName === newFileExtension[0];
const hasEmptyFilename = updatedName.trim() === '';
const hasOnlyExtension = newFileExtension && updatedName.trim() === newFileExtension[0];
if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension || hasExtensionIfFolder) {
this.props.updateFileName(this.props.id, this.originalFileName);
this.setUpdatedName(currentName);
} else {
this.saveUpdatedFileName();
}
}
toggleFileOptions(e) {
e.preventDefault();
toggleFileOptions = (event) => {
event.preventDefault();
if (!this.props.canEdit) {
return;
}
@ -93,26 +138,32 @@ export class FileNode extends React.Component {
}
}
hideFileOptions() {
hideFileOptions = () => {
this.setState({ isOptionsOpen: false });
}
showEditFileName() {
showEditFileName = () => {
this.setState({ isEditingName: true });
}
hideEditFileName() {
hideEditFileName = () => {
this.setState({ isEditingName: false });
}
renderChild(childId) {
return (
<li key={childId}>
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} />
</li>
);
showFolderChildren = () => {
this.props.showFolderChildren(this.props.id);
}
hideFolderChildren = () => {
this.props.hideFolderChildren(this.props.id);
}
renderChild = childId => (
<li key={childId}>
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} />
</li>
)
render() {
const itemClass = classNames({
'sidebar__root-item': this.props.name === 'root',
@ -123,151 +174,132 @@ export class FileNode extends React.Component {
'sidebar__file-item--closed': this.props.isFolderClosed
});
const isFile = this.props.fileType === 'file';
const isFolder = this.props.fileType === 'folder';
const isRoot = this.props.name === 'root';
return (
<div className={itemClass}>
{(() => { // eslint-disable-line
if (this.props.name !== 'root') {
return (
<div className="file-item__content" onContextMenu={this.toggleFileOptions}>
<span className="file-item__spacer"></span>
{(() => { // eslint-disable-line
if (this.props.fileType === 'file') {
return (
<span className="sidebar__file-item-icon">
<InlineSVG src={fileUrl} />
</span>
);
}
return (
<div className="sidebar__file-item--folder">
<button
className="sidebar__file-item-closed"
onClick={() => this.props.showFolderChildren(this.props.id)}
>
<InlineSVG className="folder-right" src={folderRightUrl} />
</button>
<button
className="sidebar__file-item-open"
onClick={() => this.props.hideFolderChildren(this.props.id)}
>
<InlineSVG className="folder-down" src={folderDownUrl} />
</button>
</div>
);
})()}
<button className="sidebar__file-item-name" onClick={this.handleFileClick}>{this.props.name}</button>
<input
type="text"
className="sidebar__file-item-input"
value={this.props.name}
maxLength="128"
onChange={this.handleFileNameChange}
ref={(element) => { this.fileNameInput = element; }}
onBlur={() => {
this.validateFileName();
this.hideEditFileName();
}}
onKeyPress={this.handleKeyPress}
/>
{ !isRoot &&
<div className="file-item__content" onContextMenu={this.toggleFileOptions}>
<span className="file-item__spacer"></span>
{ isFile &&
<span className="sidebar__file-item-icon">
<InlineSVG src={fileUrl} />
</span>
}
{ isFolder &&
<div className="sidebar__file-item--folder">
<button
className="sidebar__file-item-show-options"
aria-label="view file options"
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
tabIndex="0"
onClick={this.toggleFileOptions}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-closed"
onClick={this.showFolderChildren}
>
<InlineSVG src={downArrowUrl} />
<InlineSVG className="folder-right" src={folderRightUrl} />
</button>
<button
className="sidebar__file-item-open"
onClick={this.hideFolderChildren}
>
<InlineSVG className="folder-down" src={folderDownUrl} />
</button>
<div className="sidebar__file-item-options">
<ul title="file options">
{(() => { // eslint-disable-line
if (this.props.fileType === 'folder') {
return (
<li>
<button
aria-label="add file"
onClick={() => {
this.props.newFile(this.props.id);
setTimeout(() => this.hideFileOptions(), 0);
}}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Add File
</button>
</li>
);
}
})()}
{(() => { // eslint-disable-line
if (this.props.fileType === 'folder') {
return (
<li>
<button
aria-label="add folder"
onClick={() => {
this.props.newFolder(this.props.id);
setTimeout(() => this.hideFileOptions(), 0);
}}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Add Folder
</button>
</li>
);
}
})()}
<li>
<button
onClick={() => {
this.originalFileName = this.props.name;
this.showEditFileName();
setTimeout(() => this.fileNameInput.focus(), 0);
setTimeout(() => this.hideFileOptions(), 0);
}}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Rename
</button>
</li>
<li>
<button
onClick={() => {
if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) {
this.isDeleting = true;
this.props.resetSelectedFile(this.props.id);
setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100);
}
}}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Delete
</button>
</li>
</ul>
</div>
</div>
);
}
})()}
{(() => { // eslint-disable-line
if (this.props.children) {
return (
<ul className="file-item__children">
{this.props.children.map(this.renderChild)}
}
<button
className="sidebar__file-item-name"
onClick={this.handleFileClick}
>
{this.state.updatedName}
</button>
<input
type="text"
className="sidebar__file-item-input"
value={this.state.updatedName}
maxLength="128"
onChange={this.handleFileNameChange}
ref={(element) => { this.fileNameInput = element; }}
onBlur={this.handleFileNameBlur}
onKeyPress={this.handleKeyPress}
/>
<button
className="sidebar__file-item-show-options"
aria-label="view file options"
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
tabIndex="0"
onClick={this.toggleFileOptions}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
>
<InlineSVG src={downArrowUrl} />
</button>
<div className="sidebar__file-item-options">
<ul title="file options">
{ isFolder &&
<React.Fragment>
<li>
<button
aria-label="add folder"
onClick={this.handleClickAddFolder}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Create folder
</button>
</li>
<li>
<button
aria-label="add file"
onClick={this.handleClickAddFile}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Create file
</button>
</li>
{ this.props.authenticated &&
<li>
<button
aria-label="upload file"
onClick={this.handleClickUploadFile}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
>
Upload file
</button>
</li>
}
</React.Fragment>
}
<li>
<button
onClick={this.handleClickRename}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Rename
</button>
</li>
<li>
<button
onClick={this.handleClickDelete}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
className="sidebar__file-item-option"
>
Delete
</button>
</li>
</ul>
);
}
})()}
</div>
</div>
}
{ this.props.children &&
<ul className="file-item__children">
{this.props.children.map(this.renderChild)}
</ul>
}
</div>
);
}
@ -289,18 +321,21 @@ FileNode.propTypes = {
newFolder: PropTypes.func.isRequired,
showFolderChildren: PropTypes.func.isRequired,
hideFolderChildren: PropTypes.func.isRequired,
canEdit: PropTypes.bool.isRequired
canEdit: PropTypes.bool.isRequired,
openUploadFileModal: PropTypes.func.isRequired,
authenticated: PropTypes.bool.isRequired
};
FileNode.defaultProps = {
parentId: '0',
isSelectedFile: false,
isFolderClosed: false
isFolderClosed: false,
};
function mapStateToProps(state, ownProps) {
// this is a hack, state is updated before ownProps
return state.files.find(file => file.id === ownProps.id) || { name: 'test', fileType: 'file' };
const fileNode = state.files.find(file => file.id === ownProps.id) || { name: 'test', fileType: 'file' };
return Object.assign({}, fileNode, { authenticated: state.user.authenticated });
}
function mapDispatchToProps(dispatch) {

View File

@ -74,6 +74,18 @@ function KeyboardShortcutModal() {
</span>
<span>Turn off Accessible Output</span>
</li>
<li className="keyboard-shortcut-item">
<span className="keyboard-shortcut__command">
{metaKeyName} + B
</span>
<span>Toggle Sidebar</span>
</li>
<li className="keyboard-shortcut-item">
<span className="keyboard-shortcut__command">
Ctrl + `
</span>
<span>Toggle Console</span>
</li>
</ul>
);
}

View File

@ -53,7 +53,8 @@ const QuickAddList = ({
{...item}
onSelect={
(event) => {
event.target.blur();
event.stopPropagation();
event.currentTarget.blur();
handleAction(item);
}
}

View File

@ -26,12 +26,13 @@ class Searchbar extends React.Component {
handleSearchEnter = (e) => {
if (e.key === 'Enter') {
this.props.setSearchTerm(this.state.searchValue);
this.searchChange();
}
}
searchChange = (value) => {
this.props.setSearchTerm(this.state.searchValue);
searchChange = () => {
if (this.state.searchValue.trim().length === 0) return;
this.props.setSearchTerm(this.state.searchValue.trim());
};
handleSearchChange = (e) => {

View File

@ -113,19 +113,22 @@ class Sidebar extends React.Component {
Create file
</button>
</li>
<li>
<button
aria-label="upload file"
onClick={() => {
this.props.openUploadFileModal(rootFile.id);
setTimeout(this.props.closeProjectOptions, 0);
}}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
>
Upload file
</button>
</li>
{
this.props.user.authenticated &&
<li>
<button
aria-label="upload file"
onClick={() => {
this.props.openUploadFileModal(rootFile.id);
setTimeout(this.props.closeProjectOptions, 0);
}}
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
>
Upload file
</button>
</li>
}
</ul>
</div>
</div>

View File

@ -312,7 +312,7 @@ SketchListRowBase.propTypes = {
cloneProject: PropTypes.func.isRequired,
exportProjectAsZip: PropTypes.func.isRequired,
changeProjectName: PropTypes.func.isRequired,
onAddToCollection: PropTypes.func.isRequired,
onAddToCollection: PropTypes.func.isRequired
};
function mapDispatchToPropsSketchListRow(dispatch) {

View File

@ -156,6 +156,20 @@ class IDEView extends React.Component {
} else if (e.keyCode === 49 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) && e.shiftKey) {
e.preventDefault();
this.props.setAllAccessibleOutput(true);
} else if (e.keyCode === 66 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
e.preventDefault();
if (!this.props.ide.sidebarIsExpanded) {
this.props.expandSidebar();
} else {
this.props.collapseSidebar();
}
} else if (e.keyCode === 192 && e.ctrlKey) {
e.preventDefault();
if (this.props.ide.consoleIsExpanded) {
this.props.collapseConsole();
} else {
this.props.expandConsole();
}
}
}

View File

@ -327,44 +327,46 @@ class Collection extends React.Component {
</Helmet>
{this._renderLoader()}
{this.hasCollection() && this._renderCollectionMetadata()}
<div className="collection-table-wrapper">
{this._renderEmptyTable()}
{this.hasCollectionItems() &&
<table className="sketches-table" summary="table containing all collections">
<thead>
<tr>
{this._renderFieldHeader('name', 'Name')}
{this._renderFieldHeader('createdAt', 'Date Added')}
{this._renderFieldHeader('user', 'Owner')}
<th scope="col"></th>
</tr>
</thead>
<tbody>
{this.props.collection.items.map(item =>
(<CollectionItemRow
key={item.id}
item={item}
user={this.props.user}
username={this.getUsername()}
collection={this.props.collection}
/>))}
</tbody>
</table>
}
{
this.state.isAddingSketches && (
<Overlay
title="Add sketch"
actions={<SketchSearchbar />}
closeOverlay={this.hideAddSketches}
isFixedHeight
>
<div className="collection-add-sketch">
<AddToCollectionSketchList username={this.props.username} collection={this.props.collection} />
</div>
</Overlay>
)
}
<div className="collection-content">
<div className="collection-table-wrapper">
{this._renderEmptyTable()}
{this.hasCollectionItems() &&
<table className="sketches-table" summary="table containing all collections">
<thead>
<tr>
{this._renderFieldHeader('name', 'Name')}
{this._renderFieldHeader('createdAt', 'Date Added')}
{this._renderFieldHeader('user', 'Owner')}
<th scope="col"></th>
</tr>
</thead>
<tbody>
{this.props.collection.items.map(item =>
(<CollectionItemRow
key={item.id}
item={item}
user={this.props.user}
username={this.getUsername()}
collection={this.props.collection}
/>))}
</tbody>
</table>
}
{
this.state.isAddingSketches && (
<Overlay
title="Add sketch"
actions={<SketchSearchbar />}
closeOverlay={this.hideAddSketches}
isFixedHeight
>
<div className="collection-add-sketch">
<AddToCollectionSketchList username={this.props.username} collection={this.props.collection} />
</div>
</Overlay>
)
}
</div>
</div>
</section>
);
@ -378,15 +380,15 @@ Collection.propTypes = {
}).isRequired,
getCollections: PropTypes.func.isRequired,
collection: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
id: PropTypes.string,
name: PropTypes.string,
slug: PropTypes.string,
description: PropTypes.string,
owner: PropTypes.shape({
username: PropTypes.string.isRequired,
username: PropTypes.string,
}).isRequired,
items: PropTypes.arrayOf(PropTypes.shape({})),
}).isRequired,
}),
username: PropTypes.string,
loading: PropTypes.bool.isRequired,
toggleDirectionForField: PropTypes.func.isRequired,
@ -399,7 +401,14 @@ Collection.propTypes = {
};
Collection.defaultProps = {
username: undefined
username: undefined,
collection: {
id: undefined,
items: [],
owner: {
username: undefined
}
}
};
function mapStateToProps(state, ownProps) {

View File

@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import { browserHistory } from 'react-router';
import { bindActionCreators } from 'redux';
import * as CollectionsActions from '../../IDE/actions/collections';
@ -39,19 +38,7 @@ class CollectionCreate extends React.Component {
handleCreateCollection = (event) => {
event.preventDefault();
this.props.createCollection(this.state.collection)
.then(({ id, owner }) => {
const pathname = `/${owner.username}/collections/${id}`;
const location = { pathname, state: { skipSavingPath: true } };
browserHistory.replace(location);
})
.catch((error) => {
console.error('Error creating collection', error);
this.setState({
creationError: error,
});
});
this.props.createCollection(this.state.collection);
}
render() {
@ -107,12 +94,7 @@ CollectionCreate.propTypes = {
username: PropTypes.string,
authenticated: PropTypes.bool.isRequired
}).isRequired,
createCollection: PropTypes.func.isRequired,
collection: PropTypes.shape({}).isRequired, // TODO
sorting: PropTypes.shape({
field: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired
}).isRequired
createCollection: PropTypes.func.isRequired
};
function mapStateToProps(state, ownProps) {

View File

@ -72,6 +72,7 @@ function mapStateToProps(state) {
}
function mapDispatchToProps(dispatch) {
return {};
}
CollectionView.propTypes = {
@ -84,7 +85,7 @@ CollectionView.propTypes = {
}).isRequired,
theme: PropTypes.string.isRequired,
user: PropTypes.shape({
username: PropTypes.string.isRequired,
username: PropTypes.string,
}),
};

View File

@ -87,7 +87,7 @@
fill: getThemifyVariable('button-color');
opacity: 1;
}
&:enabled:hover {
&:not(disabled):hover {
border-color: getThemifyVariable('button-background-hover-color');
background-color: getThemifyVariable('button-background-hover-color');
color: getThemifyVariable('button-hover-color');
@ -95,7 +95,7 @@
fill: getThemifyVariable('button-hover-color');
}
}
&:enabled:active {
&:not(disabled):active {
border-color: getThemifyVariable('button-background-active-color');
background-color: getThemifyVariable('button-background-active-color');
color: getThemifyVariable('button-active-color');

View File

@ -186,7 +186,7 @@ $themes: (
modal-button-color: #333,
heading-text-color: #e1e1e1,
secondary-text-color: #e1e1e1,
inactive-text-color: #c1c1c1,
inactive-text-color: #f2f2f2,
background-color: #333,
button-background-color: $white,
button-color: $black,
@ -201,14 +201,14 @@ $themes: (
modal-background-color: #444,
modal-button-background-color: #C1C1C1,
modal-border-color: #949494,
icon-color: #a9a9a9,
icon-color: #d9d9d9,
icon-hover-color: $yellow,
icon-toast-hover-color: $yellow,
shadow-color: rgba(0, 0, 0, 0.16),
console-background-color: #4f4f4f,
console-color: $black,
console-header-background-color: #3f3f3f,
console-header-color: #b5b5b5,
console-header-color: #d9d9d9,
console-info-background-color: $lightsteelblue,
console-warn-background-color: $orange,
console-debug-background-color: $dodgerblue,

View File

@ -1,12 +1,15 @@
.collection-container {
padding: #{24 / $base-font-size}rem #{66 / $base-font-size}rem;
position: relative;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
flex-direction:column;
}
.collection-metadata {
width: #{1012 / $base-font-size}rem;
max-width: #{1012 / $base-font-size}rem;
width: 100%;
margin: 0 auto;
margin-bottom: #{24 / $base-font-size}rem;
}
@ -114,15 +117,28 @@
flex-grow: 0;
}
.collection-table-wrapper {
width: #{1012 / $base-font-size}rem;
margin: 0 auto;
.collection-content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
max-width: #{1012 / $base-font-size}rem;
margin: 0 auto;
width: 100%;
@include themify() {
border: 1px solid getThemifyVariable('modal-border-color');
}
}
.collection-table-wrapper {
overflow-y: auto;
max-width: 100%;
min-height: 100%;
}
// maybe don't need this?
[data-has-items=false] .collection-table-wrapper {
display: flex;
justify-content: center;

View File

@ -1,7 +1,4 @@
.CodeMirror {
@include themify() {
border: 1px solid getThemifyVariable('ide-border-color');
}
font-family: Inconsolata, monospace;
height: 100%;
}
@ -328,7 +325,10 @@ pre.CodeMirror-line {
height: calc(100% - #{29 / $base-font-size}rem);
width: 100%;
position: absolute;
&.editor-holder--hidden {
@include themify() {
border: 1px solid getThemifyVariable('ide-border-color');
}
&.editor-holder--hidden .CodeMirror {
display: none;
}
}

View File

@ -2,7 +2,7 @@
display: block;
padding-top: #{4 / $base-font-size}rem;
width: #{300 / $base-font-size}rem;
font-size: #{9 / $base-font-size}rem;
font-size: #{12 / $base-font-size}rem;
text-align: left;
@include themify() {
color: getThemifyVariable('error-color');

View File

@ -31,47 +31,47 @@ $p5-contrast-activeline: #999999;
color: $p5-contrast-white;
}
.cm-s-p5-contrast .cm-comment {
.cm-s-p5-contrast span .cm-comment {
color: $p5-contrast-lightgray;
}
.cm-s-p5-contrast .cm-def {
.cm-s-p5-contrast span .cm-def {
color: $p5-contrast-blue;
}
.cm-s-p5-contrast .cm-string {
.cm-s-p5-contrast span .cm-string {
color: $p5-contrast-green;
}
.cm-s-p5-contrast .cm-string-2 {
.cm-s-p5-contrast span .cm-string-2 {
color: $p5-contrast-green;
}
.cm-s-p5-contrast .cm-number {
.cm-s-p5-contrast span .cm-number {
color: $p5-contrast-pink;
}
.cm-s-p5-contrast .cm-keyword {
.cm-s-p5-contrast span .cm-keyword {
color: $p5-contrast-yellow;
}
.cm-s-p5-contrast .cm-variable {
.cm-s-p5-contrast span .cm-variable {
color: $p5-contrast-white;
}
.cm-s-p5-contrast .cm-variable-2 {
.cm-s-p5-contrast span .cm-variable-2 {
color: $p5-contrast-white;
}
.cm-s-p5-contrast .cm-property {
.cm-s-p5-contrast span .cm-property {
color: $p5-contrast-white;
}
.cm-s-p5-contrast .cm-atom {
.cm-s-p5-contrast span .cm-atom {
color: $p5-contrast-pink;
}
.cm-s-p5-contrast .cm-operator {
.cm-s-p5-contrast span .cm-operator {
color: $p5-contrast-lightgray;
}
@ -79,7 +79,7 @@ $p5-contrast-activeline: #999999;
color: $p5-contrast-number;
}
.cm-s-p5-contrast .CodeMirror-selected {
.cm-s-p5-contrast div .CodeMirror-selected {
background-color: $p5-contrast-selected;
}
@ -96,25 +96,25 @@ $p5-contrast-activeline: #999999;
color: #f00;
}
.cm-s-p5-contrast .CodeMirror-matchingbracket {
.cm-s-p5-contrast span .CodeMirror-matchingbracket {
outline: 1px solid $p5-contrast-lightgray;
outline-offset: 1px;
color: $p5-contrast-white !important;
}
.cm-s-p5-contrast .cm-qualifier {
.cm-s-p5-contrast span .cm-qualifier {
color: $p5-contrast-yellow;
}
.cm-s-p5-contrast .cm-tag {
.cm-s-p5-contrast span .cm-tag {
color: $p5-contrast-orange;
}
.cm-s-p5-contrast .cm-builtin {
.cm-s-p5-contrast span .cm-builtin {
color: $p5-contrast-yellow;
}
.cm-s-p5-contrast .cm-attribute {
.cm-s-p5-contrast span .cm-attribute {
color: $p5-contrast-white;
}

View File

@ -1,6 +1,9 @@
.toolbar__play-button {
@include themify() {
@extend %toolbar-button;
display: flex;
justify-content: center;
align-items: center;
&--selected {
@extend %toolbar-button--selected;
}
@ -18,8 +21,13 @@
}
}
margin-right: #{15 / $base-font-size}rem;
& span {
padding-left: #{3 / $base-font-size}rem;
span {
padding-left: #{4 / $base-font-size}rem;
display: flex;
align-items: center;
justify-content: center;
width: #{20 / $base-font-size}rem;
height: #{20 / $base-font-size}rem;
}
}
@ -30,16 +38,29 @@
.toolbar__stop-button {
@include themify() {
@extend %toolbar-button;
display: flex;
justify-content: center;
align-items: center;
margin-right: #{15 / $base-font-size}rem;
&--selected {
@extend %toolbar-button--selected;
}
}
span {
display: flex;
align-items: center;
justify-content: center;
width: #{20 / $base-font-size}rem;
height: #{20 / $base-font-size}rem;
}
}
.toolbar__preferences-button {
@include themify() {
@extend %toolbar-button;
display: flex;
justify-content: center;
align-items: center;
line-height: #{52 / $base-font-size}rem;
&--selected {
@extend %toolbar-button--selected;
@ -50,6 +71,11 @@
margin-left: auto;
& span {
padding-left: #{1 / $base-font-size}rem;
display: flex;
align-items: center;
justify-content: center;
width: #{20 / $base-font-size}rem;
height: #{20 / $base-font-size}rem;
}
}

1485
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,16 @@
"fetch-examples-ml5:prod": "cross-env NODE_ENV=production node ./dist/fetch-examples-ml5.bundle.js",
"heroku-postbuild": "touch .env; npm run build"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx}": [
"npm run lint-fix"
]
},
"jest": {
"setupFiles": [
"<rootDir>/jest.setup.js"
@ -73,7 +83,9 @@
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.18.3",
"file-loader": "^2.0.0",
"husky": "^4.2.5",
"jest": "^24.9.0",
"lint-staged": "^10.1.3",
"mini-css-extract-plugin": "^0.8.2",
"node-sass": "^4.13.1",
"nodemon": "^1.19.4",
@ -115,7 +127,7 @@
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"cross-env": "^5.2.1",
"csslint": "^0.10.0",
"csslint": "^1.0.5",
"date-fns": "^1.30.1",
"decomment": "^0.8.7",
"dotenv": "^2.0.0",

View File

@ -25,7 +25,7 @@ export function serveProject(req, res) {
const sketchDoc = window.document;
const base = sketchDoc.createElement('base');
const fullUrl = `https://${req.get('host')}${req.originalUrl}`;
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
base.href = `${fullUrl}/`;
sketchDoc.head.appendChild(base);

View File

@ -167,7 +167,7 @@ export function projectForUserExists(username, projectId, callback) {
}
function bundleExternalLibs(project, zip, callback) {
const indexHtml = project.files.find(file => file.name === 'index.html');
const indexHtml = project.files.find(file => file.name.match(/\.html$/));
let numScriptsResolved = 0;
let numScriptTags = 0;

View File

@ -7,6 +7,8 @@ const router = new Router();
router.get('/:username/sketches/:project_id/*', getProjectAsset);
router.get('/full/:project_id/*', getProjectAsset);
router.get('/:username/full/:project_id/*', getProjectAsset);
router.get('/present/:project_id/*', getProjectAsset);
router.get('/:username/present/:project_id/*', getProjectAsset);
router.get('/embed/:project_id/*', getProjectAsset);
router.get('/:username/embed/:project_id/*', getProjectAsset);

View File

@ -0,0 +1,104 @@
# p5.js 웹 에디터에 기여하기
안녕하세요! p5.js 웹 에디터에 다양한 형태로 기여해주실 분들을 환영합니다. 저희 커뮤니티에 대한 기여는 **코드 작성**에 국한되지 않으며, **버그 리포팅**, **새로운 기능 제안**, **UI/UX 디자인 제작**, **문서 업데이트** 등 여러가지 형태일 수 있습니다.
## 목차
- [p5.js 웹 에디터에 기여하기](#p5js-웹-에디터에-기여하기)
- [목차](#목차)
- [행동 수칙](#행동-수칙)
- [어떻게 기여할 수 있을까요?](#어떻게-기여할-수-있을까요)
- [첫 단계](#첫-단계)
- [좋은 첫 이슈들](#좋은-첫-이슈들)
- [좋은 중간 이슈들](#좋은-중간-이슈들)
- [프로젝트 보드](#프로젝트-보드)
- [프로젝트 아이디어](#프로젝트-아이디어)
- [이슈 검색 및 태깅](#이슈-검색-및-태깅)
- [작업 시작하기](#작업-시작하기)
- [기여 가이드](#기여-가이드)
- [커밋 메세지 쓰는 법](#커밋-메세지-쓰는-법)
- [](#팁)
## 행동 수칙
[행동 수칙](https://github.com/processing/p5.js-web-editor/blob/master/.github/CODE_OF_CONDUCT.md)에 있는 가이드라인을 따라주시기 바랍니다.
## 어떻게 기여할 수 있을까요?
만약 오픈 소스에 기여하는 게 처음이신 경우라면, [오픈 소스에 어떻게 기여할 수 있는지](https://opensource.guide/how-to-contribute/)를 읽어주시기 바랍니다.
### 첫 걸음
어디서 시작할지 모르시겠다구요? 시작점이 될 수 있을만한 몇 가지 제안들이 여기 있습니다:
* 오픈 소스 작업을 통해 무엇을 배우기를 희망하는지 생각해보시기 바랍니다. 웹 에디터는 풀스택 웹 애플리케이션이므로, 여러가지 분야가 있습니다:
- UI/UX 디자인
- 프로젝트 매니지먼트: 티켓, 풀 리퀘스트, 과업 정리
- 프런트엔드: 리액트/리덕스, CSS/사스(Sass), 코드 미러
- 백엔드: Node, Express, MongoDB, Jest, AWS
- 데브옵스: Travis CI, Jest, 도커, 쿠버네티스, AWS
* [p5.js 웹 에디터](https://editor.p5js.org)를 사용해보세요! 버그를 찾으셨나요? 이 프로젝트에 뭔가를 더하실 수 있을 것 같으신가요? 그렇다면 이슈를 열어주세요.
* 기존 이슈들을 확장시켜보세요. 가끔은 이슈들 중에 재현 과정이 미흡하거나, 솔루션 후보들이 필요한 경우들이 있습니다. 어쩔 땐 “이거 정말 중요해!” 하고 말해주는 목소리가 필요한 경우들도 있습니다.
* 여러분의 로컬 컴퓨터에서 [설치 단계](./installation.md)를 따라 프로젝트를 실행해보세요.
* [개발자 문서 디렉토리](./../../developer_docs/) 안의 문서들을 읽어보세요. 더 확장될 수 있는 뭔가가 있나요? 뭔가 빠진게 있나요?
* [개발 가이드](./development.md)를 읽어보세요.
### 좋은 첫 이슈들
처음 기여를 해보시는 분들이나, 작은 과업으로 기여를 시작하고자 하는 분들이라면, [좋은 첫 이슈들](https://github.com/processing/p5.js-web-editor/labels/good%20first%20issue) 혹은 [재현 단계들을 문서화 해야 하는 이슈들](https://github.com/processing/p5.js-web-editor/issues?q=is%3Aissue+is%3Aopen+label%3A%22needs+steps+to+reproduce%22)을 확인해보시기 바랍니다. 만약 이슈가 누구에게도 할당되지 않았다면, 여러분이 그 작업을 하셔도 좋습니다! 어떻게 이슈를 해결해야 할지 모르셔도 괜찮고, 문제를 어떻게 접근해야 할지 질문해주셔도 좋습니다! 우리는 모두 배우기 위해서, 뭔가 멋진걸 만들기 위해서 여기 있는 거니까요. 커뮤니티 멤버 중 누군가가 여러분을 도와줄 수도 있을거고, 이 이슈들은 웹 에디터, 파일 구조, 개발 과정에 대해 배울 수 있게 해주는 훌륭한 이슈들입니다.
### 좋은 중간 이슈들
좀 더 큰 규모의 일을 하고 싶다면, [좋은 중간 이슈](https://github.com/processing/p5.js-web-editor/labels/good%20medium%20issue)로 태그 되어 있는 이슈들을 살펴보시기 바랍니다. 이 이슈들은 꽤 오랜 시간이 걸릴 수 있는 독자적인 프로젝트인데, 더 깊이 관여해 기여하고 싶다면 이런 이슈들이 적당할 것입니다!
### 프로젝트 보드
많은 이슈들은 서로 연관이 있으며 더 큰 프로젝트의 일부이기도 합니다. 더 큰 그림을 보기 위해서는 [모든 프로젝트](https://github.com/processing/p5.js-web-editor/projects/4) 보드를 살펴보시기 바랍니다.
### 프로젝트 아이디어
구글 서머 오브 코드 혹은 더 큰 프로젝트를 위한 아이디어를 찾고 있다면, 프로세싱 재단 위키에 있는 [프로젝트 리스트](https://github.com/processing/processing/wiki/Project-List#p5js-web-editor)를 확인하시기 바랍니다.
### 이슈 검색 및 태깅
작업할 이슈를 찾고 있다면, [높은 우선 순위](https://github.com/processing/p5.js-web-editor/labels/priority%3Ahigh) 표시가 된 티켓들부터 시작하는 걸 권장드립니다. [기능 개선](https://github.com/processing/p5.js-web-editor/labels/type%3Afeature), [버그 수정](https://github.com/processing/p5.js-web-editor/labels/type%3Abug) 등의 태그들이 있는 티켓을 살펴보시는 것도 좋은 방법입니다.
만약 어떤 이슈가 잘못된 태그를 달고 있는 것 같다면(예: 낮은 우선 순위로 표시되어 있지만 우선 순위가 높다고 생각할 때), 이슈를 업데이트 해주시기 바랍니다!
### 작업 시작하기
어떤 이슈를 위한 작업을 시작하고 싶다면, 해당 이슈에 댓글을 달아서 관리자들에게 이를 알려 이슈를 할당 받으시기 바랍니다. 만약 다른 사람이 이미 해당 이슈에 댓글을 달았고 작업을 하기로 되어 있다면, 불필요한 중복 문제를 피하기 위해 해당 이슈와 관련된 작업을 하거나 관리자에게 묻지 않은 채로 풀 리퀘스트를 제출하는 일은 삼가주시기 바랍니다.
이제 여러분의 컴퓨터에서 프로젝트를 빌드하고 작업을 하기 위해 [설치 가이드](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/installation.md)를 따라 주시기 바랍니다.
### 기여 가이드
* [https://guides.github.com/activities/hello-world/](https://guides.github.com/activities/hello-world/)
* [https://guides.github.com/activities/forking/](https://guides.github.com/activities/forking/)
## 커밋 메세지 쓰는 법
좋은 커밋 메세지는 최소한 세 가지 중요한 목적을 충족시켜줍니다:
* 리뷰 절차의 속도를 높여줍니다.
* 좋은 릴리즈 노트를 작성하는 데에 도움을 줍니다.
* 관리자들이 어떤 변화가 왜 일어났는지 이해하기 쉽게 해줍니다.
커밋 메세지는 다음과 같은 구조로 작성해주시기 바랍니다:
```
변경 사항(#이슈-번호 키워드 포함)에 대한 짧은 (50 문자 이하) 요약
필요 시 더 자세한 설명을 담은 텍스트 추가.
72 문자 이하로 작성.
어떤 맥락에서는 첫 줄은 이메일의 제목으로,
나머지는 본문으로 간주되기도 함.
요약과 본문을 분리시키는 빈 줄은 중요함(본문이 아예 빠지지 않는 한);
분리되지 않을 경우 리베이스 같은 툴은 혼란스러워 할 수 있음.
추가 문단들은 빈 줄 이후에 온다.
- 불릿 포인트도 허용됨
- 불릿은 보통 스페이스 한 칸 다음에 하이픈이나 별표가 이용되며, 불릿 간에는 빈 줄이 있지만 이 관례는 상황에 따라 다르게 적용됨
```
* 요약과 설명의 경우 누군가에게 무언가를 명령하듯이 작성하시기 바랍니다. “Fixed”, “Added”, “Changed” 대신 “Fix”, “Add”, “Change”로 줄을 시작하십시오.
* 두 번째 줄은 항상 빈 줄로 남겨두십시오.
* 설명 란에선 최대한 자세히 서술해주십시오. 이는 커밋의 의도에 대해 더 잘 이해할 수 있게 해주고, 왜 이런 변경 사항이 있었는지 문맥을 제공해줍니다.
## 팁
* 여러분의 커밋이 무슨 일을 하는지 요약하기 어렵다면, 여러가지 논리적 변화나 버그 수정을 하나에 담고 있기 때문일 가능성이 있으며, 이런 경우에는 `git add -p `를 이용해 여러 개의 커밋으로 나누는 편이 낫습니다.

38
translations/ko/README.md Normal file
View File

@ -0,0 +1,38 @@
# [p5.js 웹 에디터](https://editor.p5js.org)
p5.js 웹 에디터는 예술가, 디자이너, 교육자, 초급자, 그 외에도 코딩을 배우고 싶어하는 모든 이들을 포함해 최대한 많은 사람들에게 코딩을 접근 가능하도록 만드는데에 초점을 맞춘 창의적 코딩을 위한 플랫폼입니다. 웹사이트만 열면 다운로드나 설정을 할 필요도 없이 곧바로 p5.js 스케치를 작성할 수 있습니다. 본 에디터는 제한적인 기능만을 제공하고 불필요한 장식을 없애는 등 단순함을 염두에 두고 만들어졌습니다. 우리는 이 에디터를 개발해나감에 있어서 커뮤니티의 의견에 귀 기울이고, 모든 변경 사항에 의도를 깃들일 수 있도록 노력하고 있습니다. 본 에디터는 무료이며 오픈 소스입니다.
우리는 또한 커뮤니티에게 최대한 많은 주인 의식과 통제권을 드리고자 노력합니다. 여러분이 작성한 스케치를 다운로드해 로컬 환경에서 스케치를 편집하는 것도 가능하며, 다른 곳에 호스팅 하는 것 역시 가능합니다. 여러분은 자신만의 에디터 버전을 호스팅해 데이터에 대한 통제권을 지닐 수도 있습니다.
## 커뮤니티
p5.js 커뮤니티에 처음 오셨나요? 그렇다면 먼저 저희의 [커뮤니티 성명서](https://p5js.org/community/)를 읽어주시기 바랍니다.
## 행동 수칙
p5.js 웹 에디터의 모든 컨트리뷰터들은 다음의 [행동 수칙](./.github/CODE_OF_CONDUCT.md)을 따라야 합니다. 우리는 친근감 있고 안전한 커뮤니티를 만들고자 노력하고 있습니다!
## 참여하기
p5.js 웹 에디터는 다수의 개인들에 의해 만들어진 협력 프로젝트이며, 여러분 역시 도움을 주실 수 있습니다. 모든 종류의 참여를 환영합니다! 더 자세한 사항을 위해서는 [기여 안내](./.github/CONTRIBUTING.md)를 확인하시기 바랍니다.
개발자 분들은 코드 기여, 버그 수정, 문서화에 대한 세부 사항을 [개발자 문서](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/)에서 확인하시기 바랍니다. 코드 작성을 시작하기 위한 좋은 시작점은 [개발 안내](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/development.md)를 살펴보는 것입니다.
## 이슈
p5.js 웹 에디터에서 버그를 발견하셨다면, [“이슈” 탭](https://github.com/processing/p5.js-web-editor/issues)에 해당 문제를 보고하실 수 있습니다.
버그와 기능 요청은 각각에 알맞은 저장소에 보고해주시기 바랍니다:
* p5.js 라이브러리와 p5.dom: [https://github.com/processing/p5.js/issues](https://github.com/processing/p5.js/issues)
* p5.accessibility: [https://github.com/processing/p5.accessibility/issues](https://github.com/processing/p5.accessibility/issues)
* p5.sound: [https://github.com/processing/p5.js-sound/issues](https://github.com/processing/p5.js-sound/issues)
* p5.js 웹사이트: [https://github.com/processing/p5.js-website/issues](https://github.com/processing/p5.js-website/issues)
## 감사의 말
본 프로젝트는 [프로세싱 재단](https://processingfoundation.org/), [뉴욕대 ITP](https://tisch.nyu.edu/itp), [뉴욕시 교육부의 CS4All](http://cs4all.nyc/)에서 후원해주셨습니다.
호스팅과 기술적 지원은 다음 단체들에서 해주셨습니다:<br />
<a href="https://www.browserstack.com/" target="_blank"><img width="100" src="https://user-images.githubusercontent.com/6063380/46976166-ab280a80-d096-11e8-983b-18dd38c8cc9b.png" /></a> <a href="https://mlab.com" target="_blank"><img width="100" src="https://user-images.githubusercontent.com/6063380/46976572-dbbc7400-d097-11e8-89fe-c7bb08ed0775.png" /></a>