diff --git a/client/constants.js b/client/constants.js index d99a9784..b3d72210 100644 --- a/client/constants.js +++ b/client/constants.js @@ -6,12 +6,13 @@ export const STOP_SKETCH = 'STOP_SKETCH'; export const OPEN_PREFERENCES = 'OPEN_PREFERENCES'; export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES'; -export const INCREASE_FONTSIZE = 'INCREASE_FONTSIZE'; -export const DECREASE_FONTSIZE = 'DECREASE_FONTSIZE'; -export const UPDATE_FONTSIZE = 'UPDATE_FONTSIZE'; +export const SET_FONT_SIZE = 'SET_FONT_SIZE'; + export const INCREASE_INDENTATION = 'INCREASE_INDENTATION'; export const DECREASE_INDENTATION = 'DECREASE_INDENTATION'; export const UPDATE_INDENTATION = 'UPDATE_INDENTATION'; +export const SET_INDENTATION = 'SET_INDENTATION'; + export const INDENT_WITH_SPACE = 'INDENT_WITH_SPACE'; export const INDENT_WITH_TAB = 'INDENT_WITH_TAB'; @@ -42,7 +43,19 @@ export const CONSOLE_EVENT = 'CONSOLE_EVENT'; export const EXPAND_CONSOLE = 'EXPAND_CONSOLE'; export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE'; + export const TOGGLE_BEEP = 'TOGGLE_BEEP'; +export const SHOW_FILE_OPTIONS = 'SHOW_FILE_OPTIONS'; +export const HIDE_FILE_OPTIONS = 'HIDE_FILE_OPTIONS'; + +export const UPDATE_FILE_NAME = 'UPDATE_FILE_NAME'; +export const DELETE_FILE = 'DELETE_FILE'; +export const SHOW_EDIT_FILE_NAME = 'SHOW_EDIT_FILE_NAME'; +export const HIDE_EDIT_FILE_NAME = 'HIDE_EDIT_FILE_NAME'; + +export const SET_AUTOSAVE = 'SET_AUTOSAVE'; +export const SET_PREFERENCES = 'SET_PREFERENCES'; + // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js index a4db7961..788871c1 100644 --- a/client/modules/IDE/actions/files.js +++ b/client/modules/IDE/actions/files.js @@ -112,3 +112,46 @@ export function createFile(formProps) { } }; } + +export function showFileOptions(fileId) { + return { + type: ActionTypes.SHOW_FILE_OPTIONS, + id: fileId + }; +} + +export function hideFileOptions(fileId) { + return { + type: ActionTypes.HIDE_FILE_OPTIONS, + id: fileId + }; +} + +export function showEditFileName(id) { + return { + type: ActionTypes.SHOW_EDIT_FILE_NAME, + id + }; +} + +export function hideEditFileName(id) { + return { + type: ActionTypes.HIDE_EDIT_FILE_NAME, + id + }; +} + +export function updateFileName(id, name) { + return { + type: ActionTypes.UPDATE_FILE_NAME, + id, + name + }; +} + +export function deleteFile(id) { + return { + type: ActionTypes.DELETE_FILE, + id + }; +} diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index 1035713a..ab8d2c5c 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -1,53 +1,102 @@ import * as ActionTypes from '../../../constants'; +import axios from 'axios'; -export function increaseFont() { - return { - type: ActionTypes.INCREASE_FONTSIZE +const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; + +function updatePreferences(formParams, dispatch) { + axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true }) + .then(() => { + }) + .catch((response) => dispatch({ + type: ActionTypes.ERROR, + error: response.data + })); +} + +export function setFontSize(value) { + return (dispatch, getState) => { // eslint-disable-line + dispatch({ + type: ActionTypes.SET_FONT_SIZE, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + fontSize: value + } + }; + updatePreferences(formParams, dispatch); + } }; } -export function decreaseFont() { - return { - 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 setIndentation(value) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_INDENTATION, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + indentationAmount: value + } + }; + updatePreferences(formParams, dispatch); + } }; } export function indentWithTab() { - return { - type: ActionTypes.INDENT_WITH_TAB + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.INDENT_WITH_TAB + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + isTabIndent: true + } + }; + updatePreferences(formParams, dispatch); + } }; } export function indentWithSpace() { - return { - type: ActionTypes.INDENT_WITH_SPACE + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.INDENT_WITH_SPACE + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + isTabIndent: false + } + }; + updatePreferences(formParams, dispatch); + } + }; +} + +export function setAutosave(value) { + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_AUTOSAVE, + value + }); + const state = getState(); + if (state.user.authenticated) { + const formParams = { + preferences: { + autosave: value + } + }; + updatePreferences(formParams, dispatch); + } }; } diff --git a/client/modules/IDE/components/Preferences.js b/client/modules/IDE/components/Preferences.js index bad64200..06e240ed 100644 --- a/client/modules/IDE/components/Preferences.js +++ b/client/modules/IDE/components/Preferences.js @@ -1,130 +1,161 @@ import React, { PropTypes } from 'react'; import InlineSVG from 'react-inlinesvg'; import classNames from 'classnames'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import * as PreferencesActions from '../actions/preferences'; +// import { bindActionCreators } from 'redux'; +// import { connect } from 'react-redux'; +// import * as PreferencesActions from '../actions/preferences'; const exitUrl = require('../../../images/exit.svg'); const plusUrl = require('../../../images/plus.svg'); const minusUrl = require('../../../images/minus.svg'); -function Preferences(props) { - const preferencesContainerClass = classNames({ - preferences: true, - '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 ( -
-
-

Preferences

- -
+class Preferences extends React.Component { + constructor(props) { + super(props); + this.handleUpdateAutosave = this.handleUpdateAutosave.bind(this); + } -
-

Text Size

- - - - -
+ handleUpdateFont(event) { + this.props.setFontSize(parseInt(event.target.value, 10)); + } -
-

Indentation Amount

- - - - -
- - + handleUpdateIndentation(event) { + this.props.setIndentation(parseInt(event.target.value, 10)); + } + + handleUpdateAutosave(event) { + const value = event.target.value === 'true'; + this.props.setAutosave(value); + } + + render() { + const preferencesContainerClass = classNames({ + preferences: true, + 'preferences--selected': this.props.isVisible + }); + let preferencesTabOptionClass = classNames({ + preference__option: true, + 'preference__option--selected': this.props.isTabIndent + }); + let preferencesSpaceOptionClass = classNames({ + preference__option: true, + 'preference__option--selected': !this.props.isTabIndent + }); + let autosaveOnClass = classNames({ + preference__option: true, + 'preference__option--selected': this.props.autosave + }); + let autosaveOffClass = classNames({ + preference__option: true, + 'preference__option--selected': !this.props.autosave + }); + return ( +
+
+

Preferences

+
-
-
- ); -} -function mapStateToProps(state) { - return { - ...state.preferences - }; -} +
+

Text Size

+ + + + +
-function mapDispatchToProps(dispatch) { - return bindActionCreators(PreferencesActions, dispatch); +
+

Indentation Amount

+ + + + +
+ + +
+
+
+

Autosave

+
+ + +
+
+ + ); + } } Preferences.propTypes = { isVisible: PropTypes.bool.isRequired, closePreferences: PropTypes.func.isRequired, - decreaseFont: PropTypes.func.isRequired, - updateFont: PropTypes.func.isRequired, fontSize: PropTypes.number.isRequired, - increaseFont: PropTypes.func.isRequired, indentationAmount: PropTypes.number.isRequired, - decreaseIndentation: PropTypes.func.isRequired, - increaseIndentation: PropTypes.func.isRequired, - updateIndentation: PropTypes.func.isRequired, + setIndentation: PropTypes.func.isRequired, indentWithSpace: PropTypes.func.isRequired, indentWithTab: PropTypes.func.isRequired, - isTabIndent: PropTypes.bool.isRequired + isTabIndent: PropTypes.bool.isRequired, + setFontSize: PropTypes.func.isRequired, + autosave: PropTypes.bool.isRequired, + setAutosave: PropTypes.func.isRequired }; -export default connect(mapStateToProps, mapDispatchToProps)(Preferences); +export default Preferences; diff --git a/client/modules/IDE/components/Sidebar.js b/client/modules/IDE/components/Sidebar.js index b680364a..47300f48 100644 --- a/client/modules/IDE/components/Sidebar.js +++ b/client/modules/IDE/components/Sidebar.js @@ -1,71 +1,89 @@ import React, { PropTypes } from 'react'; import classNames from 'classnames'; import InlineSVG from 'react-inlinesvg'; +import SidebarItem from './SidebarItem'; const rightArrowUrl = require('../../../images/right-arrow.svg'); const leftArrowUrl = require('../../../images/left-arrow.svg'); -function Sidebar(props) { - const sidebarClass = classNames({ - sidebar: true, - 'sidebar--contracted': !props.isExpanded - }); +class Sidebar extends React.Component { + constructor(props) { + super(props); + this.resetSelectedFile = this.resetSelectedFile.bind(this); + } - return ( - - ); + + + + + + + + + + ); + } } Sidebar.propTypes = { files: PropTypes.array.isRequired, - selectedFile: PropTypes.shape({ - id: PropTypes.string.isRequired - }), - setSelectedFile: PropTypes.func.isRequired + setSelectedFile: PropTypes.func.isRequired, + isExpanded: PropTypes.bool.isRequired, + newFile: PropTypes.func.isRequired, + collapseSidebar: PropTypes.func.isRequired, + expandSidebar: PropTypes.func.isRequired, + showFileOptions: PropTypes.func.isRequired, + hideFileOptions: PropTypes.func.isRequired, + deleteFile: PropTypes.func.isRequired, + showEditFileName: PropTypes.func.isRequired, + hideEditFileName: PropTypes.func.isRequired, + updateFileName: PropTypes.func.isRequired }; export default Sidebar; diff --git a/client/modules/IDE/components/SidebarItem.js b/client/modules/IDE/components/SidebarItem.js new file mode 100644 index 00000000..efec5e0a --- /dev/null +++ b/client/modules/IDE/components/SidebarItem.js @@ -0,0 +1,118 @@ +import React, { PropTypes } from 'react'; +import InlineSVG from 'react-inlinesvg'; +import classNames from 'classnames'; +const downArrowUrl = require('../../../images/down-arrow.svg'); + +class SidebarItem extends React.Component { + constructor(props) { + super(props); + this.handleKeyPress = this.handleKeyPress.bind(this); + this.handleFileNameChange = this.handleFileNameChange.bind(this); + this.validateFileName = this.validateFileName.bind(this); + } + + handleFileNameChange(event) { + this.props.updateFileName(this.props.file.id, event.target.value); + } + + handleKeyPress(event) { + console.log(event.key); + if (event.key === 'Enter') { + this.props.hideEditFileName(this.props.file.id); + } + } + + validateFileName() { + if (!this.props.file.name.match(/.*\.(js|css|html|json)$/)) { + this.props.updateFileName(this.props.file.id, this.originalFileName); + } + } + + render() { + let itemClass = classNames({ + 'sidebar__file-item': true, + 'sidebar__file-item--selected': this.props.file.isSelected, + 'sidebar__file-item--open': this.props.file.isOptionsOpen, + 'sidebar__file-item--editing': this.props.file.isEditingName + }); + + return ( +
  • this.props.hideFileOptions(this.props.file.id)} + tabIndex={this.props.fileIndex} + > + this.props.setSelectedFile(this.props.file.id)} + >{this.props.file.name} + { + this.validateFileName(); + this.props.hideEditFileName(this.props.file.id); + }} + onKeyPress={this.handleKeyPress} + /> + this.props.showFileOptions(this.props.file.id)} + > + + +
    + +
    +
  • + ); + } +} + +SidebarItem.propTypes = { + file: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + isOptionsOpen: PropTypes.bool, + isEditingName: PropTypes.bool + }).isRequired, + setSelectedFile: PropTypes.func.isRequired, + fileIndex: PropTypes.number.isRequired, + showFileOptions: PropTypes.func.isRequired, + hideFileOptions: PropTypes.func.isRequired, + deleteFile: PropTypes.func.isRequired, + resetSelectedFile: PropTypes.func.isRequired, + showEditFileName: PropTypes.func.isRequired, + hideEditFileName: PropTypes.func.isRequired, + updateFileName: PropTypes.func.isRequired +}; + +export default SidebarItem; diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index 6edb48d6..d337b503 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -13,16 +13,46 @@ import * as FileActions from '../actions/files'; import * as IDEActions from '../actions/ide'; import * as ProjectActions from '../actions/project'; import * as EditorHiddenActions from '../actions/editorHidden'; -import { getFile, getHTMLFile, getJSFiles, getCSSFiles } from '../reducers/files'; +import * as PreferencesActions from '../actions/preferences'; +import { getFile, getHTMLFile, getJSFiles, getCSSFiles, setSelectedFile } from '../reducers/files'; class IDEView extends React.Component { componentDidMount() { if (this.props.params.project_id) { const id = this.props.params.project_id; this.props.getProject(id); + + // if autosave is on and the user is the owner of the project + if (this.props.preferences.autosave + && this.props.project.owner + && this.props.project.owner.id === this.props.user.id) { + this.autosaveInterval = setInterval(this.props.saveProject, 30000); + } } } + componentDidUpdate(prevProps) { + // if user is the owner of the project + if (this.props.project.owner && this.props.project.owner.id === this.props.user.id) { + // if the user turns on autosave + // or the user saves the project for the first time + if (!this.autosaveInterval && + ((this.props.preferences.autosave && !prevProps.preferences.autosave) || + (this.props.project.id && !prevProps.project.id))) { + this.autosaveInterval = setInterval(this.props.saveProject, 30000); + // if user turns off autosave preference + } else if (this.autosaveInterval && !this.props.preferences.autosave && prevProps.preferences.autosave) { + clearInterval(this.autosaveInterval); + this.autosaveInterval = null; + } + } + } + + componentWillUnmount() { + clearInterval(this.autosaveInterval); + this.autosaveInterval = null; + } + render() { return (
    @@ -47,16 +77,30 @@ class IDEView extends React.Component {
    @@ -115,7 +159,8 @@ IDEView.propTypes = { }), getProject: PropTypes.func.isRequired, user: PropTypes.shape({ - authenticated: PropTypes.bool.isRequired + authenticated: PropTypes.bool.isRequired, + id: PropTypes.string }).isRequired, createProject: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired, @@ -130,9 +175,11 @@ IDEView.propTypes = { startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, project: PropTypes.shape({ + id: PropTypes.string, name: PropTypes.string.isRequired, owner: PropTypes.shape({ - username: PropTypes.string + username: PropTypes.string, + id: PropTypes.string }) }).isRequired, setProjectName: PropTypes.func.isRequired, @@ -144,9 +191,15 @@ IDEView.propTypes = { preferences: PropTypes.shape({ fontSize: PropTypes.number.isRequired, indentationAmount: PropTypes.number.isRequired, - isTabIndent: PropTypes.bool.isRequired + isTabIndent: PropTypes.bool.isRequired, + autosave: PropTypes.bool.isRequired }).isRequired, closePreferences: PropTypes.func.isRequired, + setFontSize: PropTypes.func.isRequired, + setIndentation: PropTypes.func.isRequired, + indentWithTab: PropTypes.func.isRequired, + indentWithSpace: PropTypes.func.isRequired, + setAutosave: PropTypes.func.isRequired, files: PropTypes.array.isRequired, updateFileContent: PropTypes.func.isRequired, selectedFile: PropTypes.shape({ @@ -166,11 +219,17 @@ IDEView.propTypes = { cloneProject: PropTypes.func.isRequired, expandConsole: PropTypes.func.isRequired, collapseConsole: PropTypes.func.isRequired, + showFileOptions: PropTypes.func.isRequired, + hideFileOptions: PropTypes.func.isRequired, + deleteFile: PropTypes.func.isRequired, + showEditFileName: PropTypes.func.isRequired, + hideEditFileName: PropTypes.func.isRequired, + updateFileName: PropTypes.func.isRequired }; function mapStateToProps(state) { return { - files: state.files, + files: setSelectedFile(state.files, state.ide.selectedFile), selectedFile: getFile(state.files, state.ide.selectedFile), htmlFile: getHTMLFile(state.files), jsFiles: getJSFiles(state.files), @@ -188,7 +247,8 @@ function mapDispatchToProps(dispatch) { EditorHiddenActions, FileActions, ProjectActions, - IDEActions), + IDEActions, + PreferencesActions), dispatch); } diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index c8568e8e..bd07aebb 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -73,11 +73,61 @@ const files = (state = initialState, action) => { return [...action.files]; case ActionTypes.CREATE_FILE: return [...state, { name: action.name, id: action.id, content: '', url: action.url }]; + case ActionTypes.SHOW_FILE_OPTIONS: + return state.map(file => { + if (file.id !== action.id) { + return file; + } + + return Object.assign({}, file, { isOptionsOpen: true }); + }); + case ActionTypes.HIDE_FILE_OPTIONS: + return state.map(file => { + if (file.id !== action.id) { + return file; + } + + return Object.assign({}, file, { isOptionsOpen: false }); + }); + case ActionTypes.UPDATE_FILE_NAME: + return state.map(file => { + if (file.id !== action.id) { + return file; + } + + return Object.assign({}, file, { name: action.name }); + }); + case ActionTypes.DELETE_FILE: + return state.filter(file => file.id !== action.id); + case ActionTypes.SHOW_EDIT_FILE_NAME: + return state.map(file => { + if (file.id !== action.id) { + return file; + } + + return Object.assign({}, file, { isEditingName: true }); + }); + case ActionTypes.HIDE_EDIT_FILE_NAME: + return state.map(file => { + if (file.id !== action.id) { + return file; + } + + return Object.assign({}, file, { isEditingName: false }); + }); default: return state; } }; +export const setSelectedFile = (state, id) => + state.map(file => { + if (file.id === id) { + return Object.assign({}, file, { isSelected: true }); + } + return file; + }); + 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 getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/)); diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index 78491b57..818dde15 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -3,35 +3,16 @@ import * as ActionTypes from '../../../constants'; const initialState = { fontSize: 18, indentationAmount: 2, - isTabIndent: true + isTabIndent: true, + autosave: true }; const preferences = (state = initialState, action) => { switch (action.type) { - case ActionTypes.INCREASE_FONTSIZE: - return Object.assign({}, state, { - fontSize: state.fontSize + 2 - }); - case ActionTypes.DECREASE_FONTSIZE: - return Object.assign({}, state, { - 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.SET_FONT_SIZE: + return Object.assign({}, state, { fontSize: action.value }); + case ActionTypes.SET_INDENTATION: + return Object.assign({}, state, { indentationAmount: action.value }); case ActionTypes.INDENT_WITH_TAB: return Object.assign({}, state, { isTabIndent: true @@ -40,6 +21,10 @@ const preferences = (state = initialState, action) => { return Object.assign({}, state, { isTabIndent: false }); + case ActionTypes.SET_AUTOSAVE: + return Object.assign({}, state, { autosave: action.value }); + case ActionTypes.SET_PREFERENCES: + return action.preferences; default: return state; } diff --git a/client/modules/Sketch/pages/SketchListView.js b/client/modules/Sketch/pages/SketchListView.js index 822a8e8d..06f18d0b 100644 --- a/client/modules/Sketch/pages/SketchListView.js +++ b/client/modules/Sketch/pages/SketchListView.js @@ -19,16 +19,20 @@ class SketchListView extends React.Component { user={this.props.user} createProject={this.props.createProject} saveProject={this.props.saveProject} + exportProjectAsZip={this.props.exportProjectAsZip} + cloneProject={this.props.cloneProject} /> - - - + + + + + {this.props.sketches.map(sketch => - + @@ -46,7 +50,9 @@ SketchListView.propTypes = { createProject: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired, getProjects: PropTypes.func.isRequired, - sketches: PropTypes.array.isRequired + sketches: PropTypes.array.isRequired, + exportProjectAsZip: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired }; function mapStateToProps(state) { diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 1b4ca040..941624b9 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -46,7 +46,13 @@ export function getUser() { type: ActionTypes.AUTH_USER, user: response.data }); + dispatch({ + type: ActionTypes.SET_PREFERENCES, + preferences: response.data.preferences + }); }) - .catch(response => dispatch(authError(response.data.error))); + .catch(response => { + dispatch(authError(response.data.error)); + }); }; } diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index 9675bb59..74bb1d70 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -95,7 +95,7 @@ %preference-option { background-color: $light-button-background-color; color: $light-inactive-text-color; - font-size: #{14 / $base-font-size}rem; + font-size: #{12 / $base-font-size}rem; cursor: pointer; text-align: left; margin-bottom: #{5 / $base-font-size}rem; diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss index fd6a3cf6..dc3899e6 100644 --- a/client/styles/components/_preferences.scss +++ b/client/styles/components/_preferences.scss @@ -61,6 +61,7 @@ margin: 0; line-height: #{20 / $base-font-size}rem; color: $light-inactive-text-color; + font-size: #{9 / $base-font-size}rem; &:hover { color: $light-inactive-text-color; } @@ -69,13 +70,22 @@ .preference__vertical-list { display: flex; flex-direction: column; + width: #{90 / $base-font-size}rem; + padding-left: #{28 / $base-font-size}rem; } .preference__option { @extend %preference-option; list-style-type: none; - padding-left: #{28 / $base-font-size}rem; + padding: 0; &--selected { @extend %preference-option--selected; } } + +.preference__options { + display: flex; + justify-content: space-between; + width: #{70 / $base-font-size}rem; +} + diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index 5dcd5725..c54ff31c 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -38,11 +38,54 @@ padding: #{8 / $base-font-size}rem #{20 / $base-font-size}rem; color: $light-inactive-text-color; cursor: pointer; + display: flex; + justify-content: space-between; + position: relative; &--selected { background-color: $ide-border-color; } } +.sidebar__file-item-name { + .sidebar__file-item--editing & { + display: none; + } +} + +.sidebar__file-item-show-options { + @extend %icon; + display: none; + .sidebar__file-item--selected & { + display: inline-block; + } +} + +.sidebar__file-item-options { + @extend %modal; + position: absolute; + top: 95%; + left: 77%; + display: none; + z-index: 100; + padding: #{8 / $base-font-size}rem #{16 / $base-font-size}rem; + background-color: $light-modal-background-color; + z-index: 100; + .sidebar__file-item--open & { + display: block; + } +} + +.sidebar__file-item-input { + display: none; + padding: 0; + border: 0; + background-color: transparent; + max-width: 90%; + .sidebar__file-item--editing & { + display: inline-block; + } +} + .sidebar__contract { @extend %icon; height: #{14 / $base-font-size}rem; diff --git a/client/styles/layout/_ide.scss b/client/styles/layout/_ide.scss index ebd2d772..52fc5e97 100644 --- a/client/styles/layout/_ide.scss +++ b/client/styles/layout/_ide.scss @@ -16,10 +16,15 @@ max-width: 45%; height: 100%; position: relative; + + // temporary fix for safari + min-height: 75vh; } .editor-holder { height: 100%; + width: 100%; + position: absolute; } .preview-frame { diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index 955b9b1b..cdbfd650 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -46,7 +46,7 @@ export function getProjects(req, res) { if (req.user) { Project.find({user: req.user._id}) // eslint-disable-line no-underscore-dangle .sort('-createdAt') - .select('name files _id createdAt updatedAt') + .select('name files id createdAt updatedAt') .exec((err, projects) => { res.json(projects); }); diff --git a/server/controllers/session.controller.js b/server/controllers/session.controller.js index f9585c0c..cbee93f4 100644 --- a/server/controllers/session.controller.js +++ b/server/controllers/session.controller.js @@ -11,7 +11,9 @@ export function createSession(req, res, next) { if (innerErr) { return next(innerErr); } return res.json({ email: req.user.email, - username: req.user.username + username: req.user.username, + preferences: req.user.preferences, + id: req.user._id }); }); })(req, res, next); @@ -21,7 +23,9 @@ export function getSession(req, res) { if (req.user) { return res.json({ email: req.user.email, - username: req.user.username + username: req.user.username, + preferences: req.user.preferences, + id: req.user._id }); } return res.status(404).send({ message: 'Session does not exist' }); diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 209e254b..488aca5f 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -22,9 +22,33 @@ export function createUser(req, res, next) { } res.json({ email: req.user.email, - username: req.user.username + username: req.user.username, + preferences: req.user.preferences, + id: req.user._id }); }); }); }); } + +export function updatePreferences(req, res) { + User.findById(req.user.id, (err, user) => { + if (err) { + return res.status(500).json({error: err}); + } + if (!user){ + return res.status(404).json({error: 'Document not found'}); + } + + const preferences = Object.assign({}, user.preferences, req.body.preferences); + user.preferences = preferences; + + user.save((err) => { + if (err) { + return res.status(500).json({error: err}); + } + + return res.json(user.preferences); + }); + }) +} diff --git a/server/models/user.js b/server/models/user.js index fc3273a8..c593159c 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -9,7 +9,12 @@ const userSchema = new Schema({ github: { type: String }, email: { type: String, unique: true }, tokens: Array, - admin: { type: Boolean, default: false } + preferences: { + fontSize: { type: Number, default: 18 }, + indentationAmount: { type: Number, default: 2 }, + isTabIndent: { type: Boolean, default: false }, + autosave: { type: Boolean, default: true } + } }, { timestamps: true }); /** @@ -28,6 +33,15 @@ userSchema.pre('save', function checkPassword(next) { // eslint-disable-line con }); }); +userSchema.virtual('id').get(function(){ + return this._id.toHexString(); +}); + +userSchema.set('toJSON', { + virtuals: true +}); + + /** * Helper method for validating user's password. */ diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index 47c26571..5dde252a 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -4,4 +4,6 @@ const router = new Router(); router.route('/signup').post(UserController.createUser); +router.route('/preferences').put(UserController.updatePreferences); + export default router;
    NameCreatedLast Updated
    NameCreatedLast Updated
    {sketch.name} {moment(sketch.createdAt).format('MMM D, YYYY')} {moment(sketch.updatedAt).format('MMM D, YYYY')}