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

View File

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

View File

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

View File

@ -30,3 +30,6 @@ export const updatePoints = createAction(types.updatePoints);
// hardGoTo(path: String) => Action
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,
points
}),
[types.updatePoints]: (state, { payload: points }) => ({
...state,
points
}),
[types.updateWindowHeight]: (state, { payload: windowHeight }) => ({
...state,
windowHeight
}),
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
...state,
navHeight
})
},
{
@ -31,6 +38,8 @@ export default handleActions(
picture: null,
points: 0,
isSignedIn: false,
csrfToken: ''
csrfToken: '',
windowHeight: 0,
navHeight: 0
}
);

View File

@ -10,5 +10,8 @@ export default createTypes([
'updatePoints',
'handleError',
// used to hit the server
'hardGoTo'
'hardGoTo',
'updateWindowHeight',
'updateNavHeight'
], '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 React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
import { push } from 'react-router-redux';
import { reduxForm } from 'redux-form';
// import debug from 'debug';
@ -106,7 +107,7 @@ function getBsStyle(field) {
'success';
}
export class NewJob extends React.Component {
export class NewJob extends PureComponent {
static displayName = 'NewJob';
static propTypes = {

View File

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

View File

@ -1,5 +1,6 @@
import React, { PropTypes } from 'react';
import { Row, Col, Thumbnail } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component';
import urlRegexFactory from 'url-regex';
const urlRegex = urlRegexFactory();
@ -18,15 +19,16 @@ function addATags(text) {
});
}
export default React.createClass({
displayName: 'ShowJob',
propTypes: {
export default class extends PureComponent {
static displayName = 'ShowJob';
static propTypes = {
job: PropTypes.object,
params: PropTypes.object,
showApply: PropTypes.bool,
preview: PropTypes.bool,
message: PropTypes.string
},
};
renderHeader({ company, position }) {
return (
@ -39,39 +41,39 @@ export default React.createClass({
</h5>
</div>
);
},
}
renderHowToApply(showApply, preview, message, howToApply) {
if (!showApply) {
return (
<Row>
<Col
md={ 6 }
mdOffset={ 3 }>
<h4 className='bg-info text-center'>{ message }</h4>
</Col>
<Col
md={ 6 }
mdOffset={ 3 }>
<h4 className='bg-info text-center'>{ message }</h4>
</Col>
</Row>
);
}
return (
<Row>
<hr />
<Col
md={ 6 }
mdOffset={ 3 }>
<div>
<bold>{ preview ? 'How do I apply?' : message }</bold>
<br />
<br />
<span dangerouslySetInnerHTML={{
__html: addATags(howToApply)
}} />
</div>
</Col>
</Row>
<Row>
<hr />
<Col
md={ 6 }
mdOffset={ 3 }>
<div>
<bold>{ preview ? 'How do I apply?' : message }</bold>
<br />
<br />
<span dangerouslySetInnerHTML={{
__html: addATags(howToApply)
}} />
</div>
</Col>
</Row>
);
},
}
render() {
const {
@ -142,4 +144,4 @@ export default React.createClass({
</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 Hikes from './Hikes';
import Challenges from './challenges';
import NotFound from '../components/NotFound/index.jsx';
export default {
@ -7,6 +8,7 @@ export default {
childRoutes: [
Jobs,
Hikes,
Challenges,
{
path: '*',
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-dom": "^15.0.2",
"react-motion": "~0.4.2",
"react-no-ssr": "^1.0.1",
"react-pure-render": "^1.0.2",
"react-redux": "^4.0.6",
"react-router": "^2.0.0",

View File

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

View File

@ -6,8 +6,8 @@ html(lang='en')
else
title Free Code Camp
include partials/react-stylesheets
body.top-and-bottom-margins(style='overflow: hidden')
.container
#fcc!= markup
body.no-top-and-bottom-margins(style='overflow: hidden')
#fcc!= markup
script!= state
script(src=rev('/js', 'vendor-challenges.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=rev('/css', 'main.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
meta(charset='utf-8')

View File

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