Add react challenge view with editor

This commit is contained in:
Berkeley Martinez
2016-03-05 21:06:04 -08:00
parent ec16357c83
commit 09ea99e561
31 changed files with 528 additions and 61 deletions

View File

@ -51,9 +51,9 @@
} }
} }
#testSuite { .challenge-test-suite {
margin-top: 10px; margin-top: 10px;
> div >.row { & .row {
margin: 0!important; margin: 0!important;
} }
} }

View File

@ -1,4 +1,4 @@
@import "lib/bootstrap/bootstrap"; @import "lib/bootstrap/bootstrap";
@import "lib/bootstrap-social/bootstrap-social"; @import "lib/bootstrap-social/bootstrap-social";
@import "lib/ionicons/ionicons"; @import "lib/ionicons/ionicons";
@import "lib/animate.min.less"; @import "lib/animate.min.less";
@ -682,21 +682,11 @@ form.update-email .btn{
padding-bottom: 117%; padding-bottom: 117%;
} }
#directions {
text-align: left;
font-size: 15px;
}
.graph-rect { .graph-rect {
fill: #ddd !important fill: #ddd !important
} }
.CodeMirror span {
/**
* Challenge styling
*/
form.code span {
font-size: 18px; font-size: 18px;
font-family: "Ubuntu Mono"; font-family: "Ubuntu Mono";
padding-bottom: 0px; padding-bottom: 0px;
@ -713,7 +703,7 @@ form.code span {
font-family: "Ubuntu Mono"; font-family: "Ubuntu Mono";
} }
#mainEditorPanel { .challenges-editor {
height: 100%; height: 100%;
width: 99%; width: 99%;
} }
@ -723,10 +713,6 @@ form.code span {
overflow-y: auto; overflow-y: auto;
} }
#mainEditorPanel .panel-body {
padding-bottom: 0px;
}
div.CodeMirror-scroll { div.CodeMirror-scroll {
padding-bottom: 30px; padding-bottom: 30px;
} }
@ -742,6 +728,11 @@ div.CodeMirror-scroll {
min-height: 650px; min-height: 650px;
} }
.challenge-log .CodeMirror {
height: 100%;
width: 100%;
}
.btn { .btn {
font-weight: 400; font-weight: 400;
white-space: normal; white-space: normal;

View File

@ -5,8 +5,13 @@ import { compose } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchUser } from './redux/actions'; import {
fetchUser,
updateWindowHeight,
updateNavHeight
} from './redux/actions';
import contain from './utils/professor-x'; import contain from './utils/professor-x';
import getWindowHeight from './utils/get-window-height';
import Nav from './components/Nav'; import Nav from './components/Nav';
@ -43,7 +48,9 @@ export class FreeCodeCamp extends React.Component {
username: PropTypes.string, username: PropTypes.string,
points: PropTypes.number, points: PropTypes.number,
picture: PropTypes.string, picture: PropTypes.string,
toast: PropTypes.object toast: PropTypes.object,
updateNavHeight: PropTypes.func,
updateWindowHeight: PropTypes.func
}; };
componentWillReceiveProps({ toast: nextToast = {} }) { componentWillReceiveProps({ toast: nextToast = {} }) {
@ -60,9 +67,13 @@ export class FreeCodeCamp extends React.Component {
} }
} }
componentDidMount() {
this.props.updateWindowHeight(getWindowHeight());
}
render() { render() {
const { username, points, picture } = this.props; const { username, points, picture, updateNavHeight } = this.props;
const navProps = { username, points, picture }; const navProps = { username, points, picture, updateNavHeight };
return ( return (
<div> <div>
@ -81,7 +92,7 @@ export class FreeCodeCamp extends React.Component {
const wrapComponent = compose( const wrapComponent = compose(
// connect Component to Redux Store // connect Component to Redux Store
connect(mapStateToProps, { fetchUser }), connect(mapStateToProps, { updateWindowHeight, updateNavHeight, fetchUser }),
// handles prefetching data // handles prefetching data
contain(fetchContainerOptions) contain(fetchContainerOptions)
); );

View File

@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { LinkContainer } from 'react-router-bootstrap'; import { LinkContainer } from 'react-router-bootstrap';
import { import {
Col, Col,
@ -35,9 +36,15 @@ export default class extends React.Component {
points: PropTypes.number, points: PropTypes.number,
picture: PropTypes.string, picture: PropTypes.string,
signedIn: PropTypes.bool, signedIn: PropTypes.bool,
username: PropTypes.string username: PropTypes.string,
updateNavHeight: PropTypes.func
}; };
componentDidMount() {
const navBar = ReactDOM.findDOMNode(this);
this.props.updateNavHeight(navBar.clientHeight);
}
renderLinks() { renderLinks() {
return navLinks.map(({ content, link, react, target }, index) => { return navLinks.map(({ content, link, react, target }, index) => {
if (react) { if (react) {

View File

@ -30,3 +30,6 @@ export const updatePoints = createAction(types.updatePoints);
// hardGoTo(path: String) => Action // hardGoTo(path: String) => Action
export const hardGoTo = createAction(types.hardGoTo); export const hardGoTo = createAction(types.hardGoTo);
export const updateWindowHeight = createAction(types.updateWindowHeight);
export const updateNavHeight = createAction(types.updateNavHeight);

View File

@ -19,10 +19,17 @@ export default handleActions(
...state, ...state,
points points
}), }),
[types.updatePoints]: (state, { payload: points }) => ({ [types.updatePoints]: (state, { payload: points }) => ({
...state, ...state,
points points
}),
[types.updateWindowHeight]: (state, { payload: windowHeight }) => ({
...state,
windowHeight
}),
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
...state,
navHeight
}) })
}, },
{ {
@ -31,6 +38,8 @@ export default handleActions(
picture: null, picture: null,
points: 0, points: 0,
isSignedIn: false, isSignedIn: false,
csrfToken: '' csrfToken: '',
windowHeight: 0,
navHeight: 0
} }
); );

View File

@ -10,5 +10,8 @@ export default createTypes([
'updatePoints', 'updatePoints',
'handleError', 'handleError',
// used to hit the server // used to hit the server
'hardGoTo' 'hardGoTo',
'updateWindowHeight',
'updateNavHeight'
], 'app'); ], 'app');

View File

@ -1 +0,0 @@
This folder contains things relative to the bonfires' screens

View File

@ -1,5 +1,6 @@
import { helpers } from 'rx'; import { helpers } from 'rx';
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
// import debug from 'debug'; // import debug from 'debug';
@ -106,7 +107,7 @@ function getBsStyle(field) {
'success'; 'success';
} }
export class NewJob extends React.Component { export class NewJob extends PureComponent {
static displayName = 'NewJob'; static displayName = 'NewJob';
static propTypes = { static propTypes = {

View File

@ -5,6 +5,10 @@ import { Button, Col, Row } from 'react-bootstrap';
export default class extends React.createClass { export default class extends React.createClass {
static displayName = 'NewJobCompleted'; static displayName = 'NewJobCompleted';
shouldComponentUpdate() {
return false;
}
render() { render() {
return ( return (
<div className='text-center'> <div className='text-center'>

View File

@ -1,5 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Row, Col, Thumbnail } from 'react-bootstrap'; import { Row, Col, Thumbnail } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component';
import urlRegexFactory from 'url-regex'; import urlRegexFactory from 'url-regex';
const urlRegex = urlRegexFactory(); const urlRegex = urlRegexFactory();
@ -18,15 +19,16 @@ function addATags(text) {
}); });
} }
export default React.createClass({ export default class extends PureComponent {
displayName: 'ShowJob', static displayName = 'ShowJob';
propTypes: {
static propTypes = {
job: PropTypes.object, job: PropTypes.object,
params: PropTypes.object, params: PropTypes.object,
showApply: PropTypes.bool, showApply: PropTypes.bool,
preview: PropTypes.bool, preview: PropTypes.bool,
message: PropTypes.string message: PropTypes.string
}, };
renderHeader({ company, position }) { renderHeader({ company, position }) {
return ( return (
@ -39,39 +41,39 @@ export default React.createClass({
</h5> </h5>
</div> </div>
); );
}, }
renderHowToApply(showApply, preview, message, howToApply) { renderHowToApply(showApply, preview, message, howToApply) {
if (!showApply) { if (!showApply) {
return ( return (
<Row> <Row>
<Col <Col
md={ 6 } md={ 6 }
mdOffset={ 3 }> mdOffset={ 3 }>
<h4 className='bg-info text-center'>{ message }</h4> <h4 className='bg-info text-center'>{ message }</h4>
</Col> </Col>
</Row> </Row>
); );
} }
return ( return (
<Row> <Row>
<hr /> <hr />
<Col <Col
md={ 6 } md={ 6 }
mdOffset={ 3 }> mdOffset={ 3 }>
<div> <div>
<bold>{ preview ? 'How do I apply?' : message }</bold> <bold>{ preview ? 'How do I apply?' : message }</bold>
<br /> <br />
<br /> <br />
<span dangerouslySetInnerHTML={{ <span dangerouslySetInnerHTML={{
__html: addATags(howToApply) __html: addATags(howToApply)
}} /> }} />
</div> </div>
</Col> </Col>
</Row> </Row>
); );
}, }
render() { render() {
const { const {
@ -142,4 +144,4 @@ export default React.createClass({
</div> </div>
); );
} }
}); }

View File

@ -0,0 +1,49 @@
import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
import { Col } from 'react-bootstrap';
import Editor from './Editor.jsx';
import SidePanel from './Side-Panel.jsx';
import Preview from './Preview.jsx';
export default class extends PureComponent {
static displayName = 'Challenge';
static propTypes = {
showPreview: PropTypes.bool
};
renderPreview(showPreview) {
if (!showPreview) {
return null;
}
return (
<Col
lg={ 3 }
md={ 5 }>
<Preview />
</Col>
);
}
render() {
const { showPreview } = this.props;
return (
<div>
<Col
lg={ 3 }
md={ showPreview ? 3 : 4 }>
<SidePanel />
</Col>
<Col
lg={ showPreview ? 6 : 9 }
md={ showPreview ? 5 : 8 }>
<Editor />
</Col>
{ this.renderPreview(showPreview) }
</div>
);
}
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import PureComponent from 'react-pure-render/component';
import Challenge from './Challenge.jsx';
export default class extends PureComponent {
static displayName = 'Challenges';
static propTypes = {};
render() {
return (
<Challenge showPreview={ false } />
);
}
}

View File

@ -0,0 +1,54 @@
import React, { PropTypes } from 'react';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import Codemirror from 'react-codemirror';
import NoSSR from 'react-no-ssr';
import PureComponent from 'react-pure-render/component';
const mapStateToProps = createSelector(
state => state.app.windowHeight,
state => state.app.navHeight,
(windowHeight, navHeight) => ({ height: windowHeight - navHeight - 50 })
);
const options = {
lint: true,
lineNumbers: true,
mode: 'javascript',
theme: 'monokai',
runnable: true,
matchBrackets: true,
autoCloseBrackets: true,
scrollbarStyle: 'null',
lineWrapping: true,
gutters: ['CodeMirror-lint-markers']
};
export class Editor extends PureComponent {
static displayName = 'Editor';
static propTypes = {
height: PropTypes.number
};
render() {
const { height } = this.props;
const style = {};
if (height) {
style.height = height + 'px';
}
return (
<div
className='challenges-editor'
style={ style }>
<NoSSR>
<Codemirror
options={ options }
value='foo test' />
</NoSSR>
</div>
);
}
}
export default connect(mapStateToProps)(Editor);

View File

@ -0,0 +1,38 @@
import React from 'react';
import PureComponent from 'react-pure-render/component';
import Codemirror from 'react-codemirror';
const defaultOutput = `/**
* Your output will go here.
* Any console.log() -type
* statements will appear in
* your browser\'s DevTools
* JavaScript console.
*/`;
const defaultOptions = {
lineNumbers: false,
mode: 'text',
theme: 'monokai',
readOnly: 'nocursor',
lineWrapping: true
};
export default class extends PureComponent {
static displayName = 'Output';
static defaultProps = {
output: defaultOutput
};
render() {
const { output } = this.props;
return (
<div className='challenge-log'>
<Codemirror
options={ defaultOptions }
value={ output } />
</div>
);
}
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import PureComponent from 'react-pure-render/component';
export default class extends PureComponent {
static displayName = 'Preview';
render() {
return (
<div>
<div className='hidden-xs hidden-md'>
<img
className='iphone-position iframe-scroll'
src='https://s3.amazonaws.com/freecodecamp/iphone6-frame.png' />
</div>
<iframe
className='iphone iframe-scroll'
id='preview' />
<div className='spacer' />
</div>
);
}
}

View File

@ -0,0 +1,101 @@
import React, { PropTypes } from 'react';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import PureComponent from 'react-pure-render/component';
import { Col, Row } from 'react-bootstrap';
import TestSuite from './Test-Suite.jsx';
import Output from './Output.jsx';
import ToolPanel from './Tool-Panel.jsx';
const mapStateToProps = createSelector(
state => state.app.windowHeight,
state => state.app.navHeight,
(windowHeight, navHeight) => ({ height: windowHeight - navHeight - 50 })
);
/* eslint-disable max-len */
const description = [
'Comments are lines of code that JavaScript will intentionally ignore. Comments are a great way to leave notes to yourself and to other people who will later need to figure out what that code does.',
'There are two ways to write comments in JavaScript:',
'Using <code>//</code> will tell JavaScript to ignore the remainder of the text on the current line:',
'<blockquote>// This is an in-line comment.</blockquote>',
'You can make a multi-line comment beginning with <code>/*</code> and ending with <code>*/</code>:',
'<blockquote>/* This is a <br> multi-line comment */</blockquote>',
'<strong>Best Practice</strong><br>As you write code, you should regularly add comments to clarify the function of parts of your code. Good commenting can help communicate the intent of your code&mdash;both for others <em>and</em> for your future self.',
'<h4>Instructions</h4>',
'Try creating one of each type of comment.'
];
/* eslint-enable max-len */
export class SidePanel extends PureComponent {
constructor(...args) {
super(...args);
this.descriptionRegex = /\<blockquote|\<ol|\<h4|\<table/;
}
static displayName = 'SidePanel';
static propTypes = {
description: PropTypes.arrayOf(PropTypes.string),
height: PropTypes.number
};
static defaultProps = {
description
};
renderDescription(description, descriptionRegex) {
return description.map((line, index) => {
if (descriptionRegex.test(line)) {
return (
<div
dangerouslySetInnerHTML={{ __html: line }}
key={ line.slice(-6) + index } />
);
}
return (
<p
className='wrappable'
dangerouslySetInnerHTML= {{ __html: line }}
key={ line.slice(-6) + index }/>
);
});
}
render() {
const { height } = this.props;
const style = {
overflowX: 'hidden',
overflowY: 'auto'
};
if (height) {
style.height = height + 'px';
}
return (
<div
ref='panel'
style={ style }>
<div>
<h4 className='text-center challenge-instructions-title'>
Build JavaScript Objects
</h4>
<hr />
<Row>
<Col
className='challenge-instructions'
xs={ 12 }>
{ this.renderDescription(description, this.descriptionRegex) }
</Col>
</Row>
</div>
<ToolPanel />
<Output />
<br />
<TestSuite />
</div>
);
}
}
export default connect(mapStateToProps)(SidePanel);

View File

@ -0,0 +1,68 @@
import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
import { Col, Row } from 'react-bootstrap';
/* eslint-disable max-len, quotes */
const tests = [{
err: null,
text: "assert((function(z){if(z.hasOwnProperty(\"name\") && z.name !== undefined && typeof z.name === \"string\"){return true;}else{return false;}})(myDog), 'message: <code>myDog</code> should contain the property <code>name</code> and it should be a <code>string</code>.');"
}, {
err: "message",
text: "assert((function(z){if(z.hasOwnProperty(\"legs\") && z.legs !== undefined && typeof z.legs === \"number\"){return true;}else{return false;}})(myDog), 'message: <code>myDog</code> should contain the property <code>legs</code> and it should be a <code>number</code>.');"
}, {
err: "message",
text: "assert((function(z){if(z.hasOwnProperty(\"tails\") && z.tails !== undefined && typeof z.tails === \"number\"){return true;}else{return false;}})(myDog), 'message: <code>myDog</code> should contain the property <code>tails</code> and it should be a <code>number</code>.');"
}, {
err: "message",
text: "assert((function(z){if(z.hasOwnProperty(\"friends\") && z.friends !== undefined && Array.isArray(z.friends)){return true;}else{return false;}})(myDog), 'message: <code>myDog</code> should contain the property <code>friends</code> and it should be an <code>array</code>.');"
}, {
err: "message",
text: "assert((function(z){return Object.keys(z).length === 4;})(myDog), 'message: <code>myDog</code> should only contain all the given properties.');"
}];
/* eslint-enable max-len, quotes */
export default class extends PureComponent {
static displayName = 'TestSuite';
static proptTypes = {
tests: PropTypes.arrayOf(PropTypes.object)
};
static defaultProps = {
tests: tests
};
renderTests(tests = []) {
return tests.map(({ err, text = '' }, index)=> {
var iconClass = err ?
'ion-close-circled big-error-icon' :
'ion-checkmark-circled big-success-icon';
return (
<Row key={ text.slice(-6) + index }>
<Col
className='text-center'
xs={ 2 }>
<i className={ iconClass } />
</Col>
<Col
className='test-output'
dangerouslySetInnerHTML={{
__html: text.split('message: ').pop().replace(/\'\);/g, '')
}}
xs={ 10 } />
</Row>
);
});
}
render() {
const { tests } = this.props;
return (
<div
className='challenge-test-suite'
style={{ marginTop: '10px' }}>
{ this.renderTests(tests) }
</div>
);
}
}

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Button, ButtonGroup } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component';
export default class extends PureComponent {
static displayName = 'ToolPanel';
render() {
return (
<div>
<Button
block={ true }
bsStyle='primary'
className='btn-big'>
Run tests (ctrl + enter)
</Button>
<div className='button-spacer' />
<ButtonGroup
className='input-group'
justified={ true }>
<Button
bsSize='large'
bsStyle='primary'
componentClass='label'>
Reset
</Button>
<Button
bsSize='large'
bsStyle='primary'
componentClass='label'>
Help
</Button>
<Button
bsSize='large'
bsStyle='primary'
componentClass='label'>
Bug
</Button>
</ButtonGroup>
<div className='button-spacer' />
</div>
);
}
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import PureComponent from 'react-pure-render/component';
export default class extends PureComponent {
render() {
return (
<div></div>
);
}
}

View File

@ -0,0 +1,6 @@
import Challenges from './components/Challenges.jsx';
export default {
path: 'challenges',
component: Challenges
};

View File

@ -1,5 +1,6 @@
import Jobs from './Jobs'; import Jobs from './Jobs';
import Hikes from './Hikes'; import Hikes from './Hikes';
import Challenges from './challenges';
import NotFound from '../components/NotFound/index.jsx'; import NotFound from '../components/NotFound/index.jsx';
export default { export default {
@ -7,6 +8,7 @@ export default {
childRoutes: [ childRoutes: [
Jobs, Jobs,
Hikes, Hikes,
Challenges,
{ {
path: '*', path: '*',
component: NotFound component: NotFound

View File

@ -0,0 +1,18 @@
export default function getWindowHeight() {
try {
const win = typeof window !== 'undefined' ?
window :
null;
if (!win) {
return 0;
}
const docElement = win.document.documentElement;
const body = win.document.getElementsByTagName('body')[0];
return win.innerHeight ||
docElement.clientHeight ||
body.clientHeight ||
0;
} catch (e) {
return 0;
}
}

View File

@ -111,6 +111,7 @@
"react-bootstrap": "~0.29.4", "react-bootstrap": "~0.29.4",
"react-dom": "^15.0.2", "react-dom": "^15.0.2",
"react-motion": "~0.4.2", "react-motion": "~0.4.2",
"react-no-ssr": "^1.0.1",
"react-pure-render": "^1.0.2", "react-pure-render": "^1.0.2",
"react-redux": "^4.0.6", "react-redux": "^4.0.6",
"react-router": "^2.0.0", "react-router": "^2.0.0",

View File

@ -13,7 +13,8 @@ const log = debug('fcc:react-server');
// remove their individual controllers // remove their individual controllers
const routes = [ const routes = [
'/videos', '/videos',
'/videos/*' '/videos/*',
'/challenges'
]; ];
const devRoutes = []; const devRoutes = [];

View File

@ -6,8 +6,8 @@ html(lang='en')
else else
title Free Code Camp title Free Code Camp
include partials/react-stylesheets include partials/react-stylesheets
body.top-and-bottom-margins(style='overflow: hidden') body.no-top-and-bottom-margins(style='overflow: hidden')
.container #fcc!= markup
#fcc!= markup
script!= state script!= state
script(src=rev('/js', 'vendor-challenges.js'))
script(src=rev('/js', 'bundle.js')) script(src=rev('/js', 'bundle.js'))

View File

@ -2,6 +2,10 @@ link(rel='stylesheet', type='text/css' href='/css/lato.css')
link(rel='stylesheet', href='/bower_components/font-awesome/css/font-awesome.min.css') link(rel='stylesheet', href='/bower_components/font-awesome/css/font-awesome.min.css')
link(rel='stylesheet', href=rev('/css', 'main.css')) link(rel='stylesheet', href=rev('/css', 'main.css'))
link(rel='stylesheet', href='/css/Vimeo.css') link(rel='stylesheet', href='/css/Vimeo.css')
link(rel='stylesheet', href='/bower_components/CodeMirror/lib/codemirror.css')
link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css')
link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css')
link(rel='stylesheet', href='/css/ubuntu.css')
include meta include meta
meta(charset='utf-8') meta(charset='utf-8')

View File

@ -31,6 +31,9 @@ module.exports = {
} }
] ]
}, },
externals: {
'codemirror': 'CodeMirror'
},
plugins: [ plugins: [
new webpack.optimize.DedupePlugin(), new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(true), new webpack.optimize.OccurenceOrderPlugin(true),