feat(nav): make navbar static (#13673)

* feat(nav): make navbar static

make the navbar in react layout and the static layout stick to the top of the screen

* feat(challenges): Make classic view flex

Classic view now uses flex to control it's height. This was necessary to control view and allow
navbar to be static on other pages.

This breaks mobile view and other non-classic challenge views

* feat(app): Add logic to make screen expand on tablet

* fix(app): let routes define their content structure

* fix(less): use American spelling of gray

* fix(classic-preview): make preview smaller to prevent scroll

* feat(classic-frame): Make frame border less distinct

* fix(challenges): scope test suite less to challenges

* feat(challenges): make generic ChallengeTitle component
This commit is contained in:
Berkeley Martinez
2017-03-13 16:17:07 -07:00
committed by Quincy Larson
parent c125c38546
commit f4443e16dd
63 changed files with 670 additions and 643 deletions

View File

@ -1,237 +0,0 @@
.challenges-instructions-panel {
clear: both;
overflow-x: hidden;
overflow-y: auto;
padding-left: 5px;
padding-right: 5px;
margin-bottom: 10px;
}
.challenge-step-description {
font-size: 1.5em;
}
.challenge-step-counter {
font-size: 20px;
line-height: 44px;
}
.challenge-step-forward-leave {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 1;
transform: translate(0, 0);
}
.challenge-step-forward-leave-active {
opacity: 0;
transform: translate(-100%, 0);
}
.challenge-step-forward-enter {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 0;
transform: translate(100%, 0);
}
.challenge-step-forward-enter-active {
opacity: 1;
transform: translate(0, 0);
}
.challenge-step-backward-leave {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 1;
transform: translate(0, 0);
}
.challenge-step-backward-leave-active {
opacity: 0;
transform: translate(100%, 0);
}
.challenge-step-backward-enter {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 0;
transform: translate(-100%, 0);
}
.challenge-step-backward-enter-active {
opacity: 1;
transform: translate(0, 0);
}
.challenge-instructions-title {
margin-top: 0;
i {
margin-left: 5px;
line-height: 20px;
}
}
.challenge-instructions {
margin-bottom: 5px;
h4 {
margin-bottom: 0;
}
blockquote {
font-size: 90%;
font-family: @font-family-monospace;
color: @code-color;
background-color: #fffbe5;
border-radius: @border-radius-base;
border: 1px solid @pre-border-color;
white-space: pre;
padding: 5px 10px;
margin-bottom: 10px;
margin-top: -5px;
overflow: auto;
}
dfn {
font-family: @font-family-monospace;
color: @code-color;
background-color: @code-bg;
border-radius: @border-radius-base;
padding: 1px 5px;
}
& a, #MDN-links a {
color: #31708f;
}
& a::after, #MDN-links a::after {
font-size: 70%;
font-family: FontAwesome;
content: " \f08e";
}
ol {
font-size: 16px;
}
}
.challenge-test-suite {
margin-top: 10px;
& .row {
margin: 0!important;
}
}
.test-output {
font-size: 15px;
font-family: "Ubuntu Mono";
margin-top: 8px;
line-height:20px;
word-wrap: break-word;
}
.grayed-out-test-output {
color: @gray-light;
}
.big-icon {
font-size: 30px;
}
.error-icon {
color: @brand-danger;
top: 50%;
}
.success-icon {
color: @brand-primary;
}
.refresh-icon {
color: @icon-grey;
}
.night {
.challenge-instructions blockquote {
background-color: #242424;
border-color: #515151;
color: #ABABAB
}
.challenge-instructions dfn {
background-color: #242424;
color: #02a902;
}
.CodeMirror {
background-color:#242424;
color:#ABABAB;
&-gutters {
background-color:#242424;
color:#ABABAB;
}
.cm-bracket, .cm-tag {
color:#5CAFD6;
}
.cm-property, .cm-string {
color:#B5753A;
}
.cm-keyword, .cm-attribute {
color:#9BBBDC;
}
}
.refresh-icon {
color: @icon-light-grey;
}
}
.challenges-editor {
height: auto;
width: 100%;
overflow-y: auto;
}
.challenges-preview {
clear: both;
overflow: hidden;
}
@media only screen and (min-width: 1031px) {
.iframe-scroll {
z-index: 1;
}
}
@media only screen and (max-width: 1030px) {
.iframe-scroll {
height: auto;
overflow: auto;
}
.iphone-position {
display: none;
}
}
iframe.iphone {
border: none;
@media(min-width: 1031px) {
width: 280px;
height: 497px;
position: absolute;
top: 75px;
right: 35px;
overflow-y: scroll;
}
@media(max-width: 1030px) {
width: 100%;
border-radius: 5px;
overflow-y: visible;
height: 500px;
}
@media (min-width: 1200px) and (max-width: 1250px){
right: 22px;
}
}
// To adjust right margin, negative values bring the image closer to the edge of the screen
.iphone-position {
position: absolute;
top: -45px;
z-index: -1;
right: -195px;
@media (min-width: 1200px) and (max-width: 1250px){
right: -207px;
}
}
// YouTube embed
.embed-responsive-item {
max-width: 100%;
}

117
client/less/flexgrid.less Normal file
View File

@ -0,0 +1,117 @@
.justify-mixin(start) { justify-content: flex-start }
.justify-mixin(end) { justify-content: flex-end }
.justify-mixin(center) { justify-content: center }
.justify-mixin(around) { justify-content: space-around }
.justify-mixin(between) { justify-content: between }
.justify-mixin(@_) {}
.items-mixin(top) { align-items: flex-start; }
.items-mixin(bottom) { align-items: flex-end; }
.items-mixin(center) { align-items: center; }
.items-mixin(stretch) { align-items: stretch; }
.items-mixin(baseline) { align-items: baseline; }
.items-mixin(@_) {}
.item-mixin(top) { align-self: flex-start; }
.item-mixin(bottom) { align-self: flex-end; }
.item-mixin(center) { align-self: center; }
.item-mixin(stretch) { align-self: stretch; }
.item-mixin(baseline) { align-self: baseline; }
.item-mixin(@_) {}
.content-mixin(top) { align-content: flex-start; }
.content-mixin(bottom) { align-content: flex-end; }
.content-mixin(center) { align-content: center; }
.content-mixin(stretch) { align-content: stretch; }
.content-mixin(baseline) { align-content: baseline; }
.content-mixin(@_) {}
.grid(@direction: row; @items: none; @justify: none; @content: none) {
display: flex;
flex-wrap: wrap;
flex-direction: @direction;
.justify-mixin(@justify);
.items-mixin(@item);
.content-mixin(@content);
}
.row(@items: none; @justify: none; @content: none) {
.grid(@direction: row, @items, @justify, @content);
}
.column(@items: none; @justify: none; @content: none) {
.grid(@direction: column; @items; @justify; @content);
}
.margin-mixin(@g) when not (@g = 0) {
margin: @g / 2;
}
.cell(@i: 1; @item; @g: @grid-gutter-width; @cols: @grid-columns) {
flex-basis: %('calc(100% * %s - %s)', @i / @cols, @g);
min-width: 0; // FF adjustment for responsive images
.item-mixin(@item);
.margin-mixin(@g);
}
.cell-offset(@i: 1; @g: @grid-gutter-width; @cols: @grid-columns) {
margin-left: e%('calc(100% * %s + %s)', @i / @cols, @g / 2) !important;
}
.padding-mixin(@pad) when (@pad) {
padding-left: @pad;
padding-right: @pad;
}
// center an element
.center(@value: 50%; @padding: 0) {
margin-left: auto;
margin-right: auto;
max-width: @value;
width: 100%;
.padding-mixin(@padding);
}
.between(@min; @max; @rules) {
// BS logic to do string building with conditions
.condition-wrapper(@new) {
.redefine-condition() {
@condition: @new;
}
}
.init-condition() {
.condition-wrapper('only screen');
}
.init-condition();
.add-min-media(@min) when (iskeyword(@min)) {
.redefine-condition();
.condition-wrapper(%('%s and (min-width: @{screen-%s})', @condition, @min));
}
.add-max-media(@max) when (iskeyword(@max)) {
.redefine-condition();
.condition-wrapper(%('%s and (max-width: @{screen-%s})', @condition, @max));
}
.add-min-media(@min);
.add-max-media(@max);
.add-query() {
.redefine-condition();
@query: e(@condition);
@media @query {
@rules();
}
}
.add-query();
}
.below(@max, @rules) {
.between(@empty; @max; @rules);
}
.above(@min, @rules) {
.between(@min; @empty; @rules);
}

View File

@ -1,6 +1,7 @@
// //
// Variables // Variables
// -------------------------------------------------- // --------------------------------------------------
@empty: ~'';
//== Colors //== Colors
@ -20,8 +21,8 @@
@brand-warning: #f0ad4e; @brand-warning: #f0ad4e;
@brand-danger: #d9534f; @brand-danger: #d9534f;
@icon-grey: #575757; @icon-gray: #575757;
@icon-light-grey: #888888; @icon-light-gray: #888888;
//== Scaffolding //== Scaffolding
// //
//## Settings for some of the most global styles. //## Settings for some of the most global styles.
@ -305,6 +306,10 @@
//** Deprecated `@screen-lg-desktop` as of v3.0.1 //** Deprecated `@screen-lg-desktop` as of v3.0.1
@screen-lg-desktop: @screen-lg-min; @screen-lg-desktop: @screen-lg-min;
// Large screen / wide desktop
@screen-xl: 1400px;
@screen-lg-min: @screen-lg;
// So media queries don't overlap when required, provide a maximum // So media queries don't overlap when required, provide a maximum
@screen-xs-max: (@screen-sm-min - 1); @screen-xs-max: (@screen-sm-min - 1);
@screen-sm-max: (@screen-md-min - 1); @screen-sm-max: (@screen-md-min - 1);
@ -345,6 +350,8 @@
//** For `@screen-lg-min` and up. //** For `@screen-lg-min` and up.
@container-lg: @container-large-desktop; @container-lg: @container-large-desktop;
@container-xl: (1340px + @grid-gutter-width);
//== Navbar //== Navbar
// //

View File

@ -1,8 +1,9 @@
@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"; @import "./lib/animate";
@import "lib/bootstrap/variables"; @import "./lib/bootstrap/variables";
@import "./flexgrid.less";
html,body,div,span,a,li,td,th { html,body,div,span,a,li,td,th {
font-family: 'Lato', sans-serif; font-family: 'Lato', sans-serif;
@ -34,13 +35,6 @@ pre.wrappable {
word-wrap: break-word; /* IE 5+ */ word-wrap: break-word; /* IE 5+ */
} }
html {
position: relative;
min-height: 100%;
// hack to prevent horizontal overflow problem on showHTML view
overflow-x: hidden;
}
//input[type=checkbox] { //input[type=checkbox] {
// /* Double-sized Checkboxes */ // /* Double-sized Checkboxes */
// -ms-transform: scale(2); /* IE */ // -ms-transform: scale(2); /* IE */
@ -54,28 +48,14 @@ html {
border-color: @brand-primary; border-color: @brand-primary;
} }
body.full-screen-body-background { .full-screen-body-background {
background-color: @body-bg; background-color: @body-bg;
} }
body.top-and-bottom-margins { .no-top-and-bottom-margins {
padding-top: 80px;
margin-bottom: 60px;
}
body.no-top-and-bottom-margins {
margin: 75px 20px 0px 20px; margin: 75px 20px 0px 20px;
} }
body.react-layout {
margin-top: 75px;
margin-bottom: 15px;
width: auto;
padding-left: 15px;
padding-right: 15px;
min-height: 650px;
}
h1, h2 { h1, h2 {
font-weight: 400; font-weight: 400;
} }
@ -165,9 +145,12 @@ h1, h2, h3, h4, h5, h6, p, li {
} }
} }
// defined in bootstrap
@navbar-total-height: @navbar-height + @navbar-margin-bottom;
.nav-height { .nav-height {
height: 50px;
border: none; border: none;
height: @navbar-height;
width: 100%;
} }
.landing-icon { .landing-icon {
@ -1241,11 +1224,14 @@ and (max-width : 400px) {
} }
} }
// surrounding downstream import with &{}
@import "code-mirror.less"; // creates locally scoped imports
@import "challenge.less"; // and prevents vaiables from overwriting each other
@import "toastr.less"; &{ @import "./code-mirror.less"; }
@import "map.less"; &{ @import "./challenge.less"; }
@import "sk-wave.less"; &{ @import "./toastr.less"; }
@import "classic-modal.less"; &{ @import "./map.less"; }
@import "skeleton-shimmer.less"; &{ @import "./sk-wave.less"; }
&{ @import "./classic-modal.less"; }
&{ @import "./skeleton-shimmer.less"; }
&{ @import "../../common/index.less"; }

View File

@ -54,27 +54,10 @@
width: 100%; width: 100%;
} }
.map-fixed-header {
position: fixed;
background: white;
width: 100%;
padding-top: 5px;
z-index: 1;
left: 0;
top: 0;
}
.drawer .map-fixed-header {
padding-top: 30px;
position: static;
margin-bottom: -100px;
}
.map-accordion { .map-accordion {
width: 100%;
margin-top: 100px;
max-width: 700px; max-width: 700px;
overflow-y: auto; overflow-y: auto;
width: 100%;
.map-accordion-panel-title { .map-accordion-panel-title {
padding-bottom: 0px; padding-bottom: 0px;

View File

@ -7,7 +7,6 @@ import hardGoToSaga from './hard-go-to-saga.js';
import mouseTrapSaga from './mouse-trap-saga.js'; import mouseTrapSaga from './mouse-trap-saga.js';
import nightModeSaga from './night-mode-saga.js'; import nightModeSaga from './night-mode-saga.js';
import titleSaga from './title-saga.js'; import titleSaga from './title-saga.js';
import windowSaga from './window-saga.js';
export default [ export default [
analyticsSaga, analyticsSaga,
@ -18,6 +17,5 @@ export default [
hardGoToSaga, hardGoToSaga,
mouseTrapSaga, mouseTrapSaga,
nightModeSaga, nightModeSaga,
titleSaga, titleSaga
windowSaga
]; ];

View File

@ -1,35 +0,0 @@
import { Observable } from 'rx';
import types from '../../common/app/redux/types';
import { updateWindowHeight } from '../../common/app/redux/actions';
const { initWindowHeight } = types;
function getWindowSize(document, window) {
const body = document.getElementsByTagName('body')[0];
return window.innerHeight ||
document.docElement.clientHeight ||
body.clientHeight ||
0;
}
function listenForResize(document, window) {
return Observable.fromEvent(window, 'resize')
.debounce(250)
.startWith({})
.map(() => getWindowSize(document, window));
}
export default function windowSaga(
action$,
getState,
{ isDev, document, window }
) {
return action$
.filter(({ type }) => type === initWindowHeight)
.flatMap(() => {
if (isDev) {
return listenForResize(document, window);
}
return Observable.just(getWindowSize(document, window));
})
.map(updateWindowHeight);
}

View File

@ -1,12 +1,11 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Button, Row } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import ns from './ns.json';
import { import {
fetchUser, fetchUser,
initWindowHeight,
updateNavHeight,
updateAppLang, updateAppLang,
trackEvent, trackEvent,
loadCurrentChallenge, loadCurrentChallenge,
@ -21,15 +20,13 @@ import Toasts from './toasts/Toasts.jsx';
import { userSelector } from './redux/selectors'; import { userSelector } from './redux/selectors';
const mapDispatchToProps = { const mapDispatchToProps = {
initWindowHeight, closeDropdown,
updateNavHeight,
fetchUser, fetchUser,
submitChallenge,
updateAppLang,
trackEvent,
loadCurrentChallenge, loadCurrentChallenge,
openDropdown, openDropdown,
closeDropdown submitChallenge,
trackEvent,
updateAppLang
}; };
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
@ -58,7 +55,6 @@ const propTypes = {
children: PropTypes.node, children: PropTypes.node,
closeDropdown: PropTypes.func.isRequired, closeDropdown: PropTypes.func.isRequired,
fetchUser: PropTypes.func, fetchUser: PropTypes.func,
initWindowHeight: PropTypes.func,
isNavDropdownOpen: PropTypes.bool, isNavDropdownOpen: PropTypes.bool,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
loadCurrentChallenge: PropTypes.func.isRequired, loadCurrentChallenge: PropTypes.func.isRequired,
@ -71,7 +67,6 @@ const propTypes = {
toast: PropTypes.object, toast: PropTypes.object,
trackEvent: PropTypes.func.isRequired, trackEvent: PropTypes.func.isRequired,
updateAppLang: PropTypes.func.isRequired, updateAppLang: PropTypes.func.isRequired,
updateNavHeight: PropTypes.func,
username: PropTypes.string username: PropTypes.string
}; };
@ -84,7 +79,6 @@ export class FreeCodeCamp extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.props.initWindowHeight();
if (!this.props.isSignedIn) { if (!this.props.isSignedIn) {
this.props.fetchUser(); this.props.fetchUser();
} }
@ -110,7 +104,6 @@ export class FreeCodeCamp extends React.Component {
username, username,
points, points,
picture, picture,
updateNavHeight,
trackEvent, trackEvent,
loadCurrentChallenge, loadCurrentChallenge,
openDropdown, openDropdown,
@ -118,23 +111,22 @@ export class FreeCodeCamp extends React.Component {
isNavDropdownOpen isNavDropdownOpen
} = this.props; } = this.props;
const navProps = { const navProps = {
username, closeDropdown,
points, isNavDropdownOpen,
picture,
updateNavHeight,
trackEvent,
loadCurrentChallenge, loadCurrentChallenge,
openDropdown, openDropdown,
closeDropdown, picture,
isNavDropdownOpen points,
trackEvent,
username
}; };
return ( return (
<div> <div className={ `${ns}-container` }>
<Nav { ...navProps }/> <Nav { ...navProps }/>
<Row> <div className={ `${ns}-content` }>
{ this.props.children } { this.props.children }
</Row> </div>
<Toasts /> <Toasts />
</div> </div>
); );

14
common/app/app.less Normal file
View File

@ -0,0 +1,14 @@
// should match ./ns.json value and filename
@ns: app;
.@{ns}-container {
.column();
width: 100vw;
}
.@{ns}-content {
.center(@value: @container-xl, @padding: @grid-gutter-width);
// makes the inital content height 0px
// then lets it grow to fit the rest of the space
flex: 1 0 0px;
}

View File

@ -1,5 +1,4 @@
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,
@ -41,7 +40,6 @@ const propTypes = {
showLoading: PropTypes.bool, showLoading: PropTypes.bool,
signedIn: PropTypes.bool, signedIn: PropTypes.bool,
trackEvent: PropTypes.func.isRequired, trackEvent: PropTypes.func.isRequired,
updateNavHeight: PropTypes.func,
username: PropTypes.string username: PropTypes.string
}; };
@ -55,11 +53,6 @@ export default class FCCNav extends React.Component {
}); });
} }
componentDidMount() {
const navBar = ReactDOM.findDOMNode(this);
this.props.updateNavHeight(navBar.clientHeight);
}
handleMapClickOnMap(e) { handleMapClickOnMap(e) {
e.preventDefault(); e.preventDefault();
this.props.trackEvent({ this.props.trackEvent({
@ -172,7 +165,7 @@ export default class FCCNav extends React.Component {
return ( return (
<Navbar <Navbar
className='nav-height' className='nav-height'
fixedTop={ true } staticTop={ true }
> >
<Navbar.Header> <Navbar.Header>
<Navbar.Toggle children={ toggleButtonChild } /> <Navbar.Toggle children={ toggleButtonChild } />

2
common/app/index.less Normal file
View File

@ -0,0 +1,2 @@
&{ @import "./app.less"; }
&{ @import "./routes/index.less"; }

1
common/app/ns.json Normal file
View File

@ -0,0 +1 @@
"app"

View File

@ -113,11 +113,6 @@ export const delayedRedirect = createAction(types.delayedRedirect);
// hardGoTo(path: String) => Action // hardGoTo(path: String) => Action
export const hardGoTo = createAction(types.hardGoTo); export const hardGoTo = createAction(types.hardGoTo);
export const initWindowHeight = createAction(types.initWindowHeight);
export const updateWindowHeight = createAction(types.updateWindowHeight);
export const updateNavHeight = createAction(types.updateNavHeight);
// data // data
export const updateChallengesData = createAction(types.updateChallengesData); export const updateChallengesData = createAction(types.updateChallengesData);
export const updateHikesData = createAction(types.updateHikesData); export const updateHikesData = createAction(types.updateHikesData);

View File

@ -7,8 +7,6 @@ const initialState = {
user: '', user: '',
lang: '', lang: '',
csrfToken: '', csrfToken: '',
windowHeight: 0,
navHeight: 0,
theme: 'default' theme: 'default'
}; };
@ -41,14 +39,6 @@ export default handleActions(
...state, ...state,
points points
}), }),
[types.updateWindowHeight]: (state, { payload: windowHeight }) => ({
...state,
windowHeight
}),
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
...state,
navHeight
}),
[types.delayedRedirect]: (state, { payload }) => ({ [types.delayedRedirect]: (state, { payload }) => ({
...state, ...state,
delayedRedirect: payload delayedRedirect: payload

View File

@ -22,10 +22,6 @@ export default createTypes([
'hardGoTo', 'hardGoTo',
'delayedRedirect', 'delayedRedirect',
'initWindowHeight',
'updateWindowHeight',
'updateNavHeight',
// data handling // data handling
'updateChallengesData', 'updateChallengesData',
'updateHikesData', 'updateHikesData',

View File

@ -2,10 +2,13 @@ import React, { PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Button, Modal } from 'react-bootstrap'; import { Button, Modal } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import { createIssue, openIssueSearch, closeBugModal } from '../redux/actions';
import ns from './ns.json';
import { createIssue, openIssueSearch, closeBugModal } from './redux/actions';
const mapStateToProps = state => ({ isOpen: state.challengesApp.isBugOpen }); const mapStateToProps = state => ({ isOpen: state.challengesApp.isBugOpen });
const actions = { createIssue, openIssueSearch, closeBugModal }; const mapDispatchToProps = { createIssue, openIssueSearch, closeBugModal };
const bugLink = 'http://forum.freecodecamp.com/t/how-to-report-a-bug/19543'; const bugLink = 'http://forum.freecodecamp.com/t/how-to-report-a-bug/19543';
const propTypes = { const propTypes = {
@ -16,7 +19,6 @@ const propTypes = {
}; };
export class BugModal extends PureComponent { export class BugModal extends PureComponent {
render() { render() {
const { const {
isOpen, isOpen,
@ -28,7 +30,7 @@ export class BugModal extends PureComponent {
<Modal <Modal
show={ isOpen } show={ isOpen }
> >
<Modal.Header className='challenge-list-header'> <Modal.Header className={ `${ns}-list-header` }>
Did you find a bug? Did you find a bug?
<span <span
className='close closing-x' className='close closing-x'
@ -85,4 +87,4 @@ export class BugModal extends PureComponent {
BugModal.displayName = 'BugModal'; BugModal.displayName = 'BugModal';
BugModal.propTypes = propTypes; BugModal.propTypes = propTypes;
export default connect(mapStateToProps, actions)(BugModal); export default connect(mapStateToProps, mapDispatchToProps)(BugModal);

View File

@ -0,0 +1,30 @@
import React, { PropTypes } from 'react';
import ns from './ns.json';
const propTypes = {
children: PropTypes.string,
isCompleted: PropTypes.bool
};
export default function ChallengeTitle({ children, isCompleted }) {
let icon = null;
if (isCompleted) {
icon = (
<i
className='ion-checkmark-circled text-primary'
title='Completed'
/>
);
}
return (
<h4 className={ `text-center ${ns}-title` }>
{ children || 'Happy Coding!' }
{ icon }
<hr />
</h4>
);
}
ChallengeTitle.displayName = 'ChallengeTitle';
ChallengeTitle.propTypes = propTypes;

View File

@ -5,6 +5,7 @@ import { Grid, Col, Row } from 'react-bootstrap';
const propTypes = { const propTypes = {
content: PropTypes.string content: PropTypes.string
}; };
export default class CodeMirrorSkeleton extends PureComponent { export default class CodeMirrorSkeleton extends PureComponent {
renderLine(line, i) { renderLine(line, i) {

View File

@ -2,14 +2,15 @@ import React, { PureComponent, PropTypes } from 'react';
import NoSSR from 'react-no-ssr'; import NoSSR from 'react-no-ssr';
import Codemirror from 'react-codemirror'; import Codemirror from 'react-codemirror';
import CodeMirrorSkeleton from './CodeMirrorSkeleton.jsx'; import ns from './ns.json';
import CodeMirrorSkeleton from './Code-Mirror-Skeleton.jsx';
const defaultOptions = { const defaultOptions = {
lineNumbers: false, lineNumbers: false,
lineWrapping: true,
mode: 'javascript', mode: 'javascript',
theme: 'monokai',
readOnly: 'nocursor', readOnly: 'nocursor',
lineWrapping: true theme: 'monokai'
}; };
const propTypes = { const propTypes = {
@ -21,7 +22,7 @@ export default class Output extends PureComponent {
render() { render() {
const { output, defaultOutput } = this.props; const { output, defaultOutput } = this.props;
return ( return (
<div className='challenge-log'> <div className={ `${ns}-log` }>
<NoSSR onSSR={ <CodeMirrorSkeleton content={ output } /> }> <NoSSR onSSR={ <CodeMirrorSkeleton content={ output } /> }>
<Codemirror <Codemirror
options={ defaultOptions } options={ defaultOptions }

View File

@ -5,32 +5,32 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import Classic from './classic/Classic.jsx'; import Classic from './views/classic';
import Step from './step/Step.jsx'; import Step from './views/step';
import Project from './project/Project.jsx'; import Project from './views/project';
import Video from './video/Video.jsx'; import Video from './views/video';
import BackEnd from './backend/Back-End.jsx'; import BackEnd from './views/backend';
import { import {
fetchChallenge, fetchChallenge,
fetchChallenges, fetchChallenges,
replaceChallenge, replaceChallenge,
resetUi resetUi
} from '../redux/actions'; } from './redux/actions';
import { challengeSelector } from '../redux/selectors'; import { challengeSelector } from './redux/selectors';
import { updateTitle } from '../../../redux/actions'; import { updateTitle } from '../../redux/actions';
import { makeToast } from '../../../toasts/redux/actions'; import { makeToast } from '../../toasts/redux/actions';
const views = { const views = {
step: Step, backend: BackEnd,
classic: Classic, classic: Classic,
project: Project, project: Project,
simple: Project, simple: Project,
video: Video, step: Step,
backend: BackEnd video: Video
}; };
const bindableActions = { const mapDispatchToProps = {
fetchChallenge, fetchChallenge,
fetchChallenges, fetchChallenges,
makeToast, makeToast,
@ -78,21 +78,21 @@ const link = 'http://forum.freecodecamp.com/t/' +
'-to-any-language/19111'; '-to-any-language/19111';
const propTypes = { const propTypes = {
areChallengesLoaded: PropTypes.bool, areChallengesLoaded: PropTypes.bool,
fetchChallenges: PropTypes.func.isRequired, fetchChallenges: PropTypes.func.isRequired,
isStep: PropTypes.bool, isStep: PropTypes.bool,
isTranslated: PropTypes.bool, isTranslated: PropTypes.bool,
lang: PropTypes.string.isRequired, lang: PropTypes.string.isRequired,
makeToast: PropTypes.func.isRequired, makeToast: PropTypes.func.isRequired,
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
replaceChallenge: PropTypes.func.isRequired, replaceChallenge: PropTypes.func.isRequired,
resetUi: PropTypes.func.isRequired, resetUi: PropTypes.func.isRequired,
title: PropTypes.string, title: PropTypes.string,
updateTitle: PropTypes.func.isRequired, updateTitle: PropTypes.func.isRequired,
viewType: PropTypes.string viewType: PropTypes.string
}; };
export class Challenges extends PureComponent { export class Show extends PureComponent {
componentWillMount() { componentWillMount() {
const { lang, isTranslated, makeToast } = this.props; const { lang, isTranslated, makeToast } = this.props;
@ -137,25 +137,17 @@ export class Challenges extends PureComponent {
} }
} }
renderView(viewType) { render() {
const { viewType } = this.props;
const View = views[viewType] || Classic; const View = views[viewType] || Classic;
return <View />; return <View />;
} }
render() {
const { viewType } = this.props;
return (
<div>
{ this.renderView(viewType) }
</div>
);
}
} }
Challenges.displayName = 'Challenges'; Show.displayName = 'Show(ChallengeView)';
Challenges.propTypes = propTypes; Show.propTypes = propTypes;
export default compose( export default compose(
connect(mapStateToProps, bindableActions), connect(mapStateToProps, mapDispatchToProps),
contain(fetchOptions) contain(fetchOptions)
)(Challenges); )(Show);

View File

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { HelpBlock, FormGroup, FormControl } from 'react-bootstrap'; import { HelpBlock, FormGroup, FormControl } from 'react-bootstrap';
import { getValidationState, DOMOnlyProps } from '../../../utils/form'; import { getValidationState, DOMOnlyProps } from '../../utils/form';
const propTypes = { const propTypes = {
placeholder: PropTypes.string, placeholder: PropTypes.string,

View File

@ -2,6 +2,8 @@ import React, { PropTypes, PureComponent } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { Col, Row } from 'react-bootstrap'; import { Col, Row } from 'react-bootstrap';
import ns from './ns.json';
const propTypes = { const propTypes = {
tests: PropTypes.arrayOf(PropTypes.object) tests: PropTypes.arrayOf(PropTypes.object)
}; };
@ -41,7 +43,7 @@ export default class TestSuite extends PureComponent {
const { tests } = this.props; const { tests } = this.props;
return ( return (
<div <div
className='challenge-test-suite' className={ `${ns}-test-suite` }
style={{ marginTop: '10px' }} style={{ marginTop: '10px' }}
> >
{ this.renderTests(tests) } { this.renderTests(tests) }

View File

@ -0,0 +1,81 @@
// should be the same as the filename and ./ns.json
@ns: challenges;
.@{ns}-title {
margin-top: 0;
i {
margin-left: 5px;
line-height: 20px;
}
}
.@{ns}-grayed-out-test-output {
color: @gray-light;
}
.@{ns}-test-suite {
margin-top: 10px;
& .row {
margin: 0!important;
}
.big-icon {
font-size: 30px;
}
.error-icon {
color: @brand-danger;
top: 50%;
}
.success-icon {
color: @brand-primary;
}
.refresh-icon {
color: @icon-gray;
}
}
.night {
.@{ns}-instructions blockquote {
background-color: #242424;
border-color: #515151;
color: #ABABAB
}
.@{ns}-instructions dfn {
background-color: #242424;
color: #02a902;
}
.@{ns}-editor .CodeMirror {
background-color:#242424;
color:#ABABAB;
&-gutters {
background-color:#242424;
color:#ABABAB;
}
.cm-bracket, .cm-tag {
color:#5CAFD6;
}
.cm-property, .cm-string {
color:#B5753A;
}
.cm-keyword, .cm-attribute {
color:#9BBBDC;
}
}
.refresh-icon {
color: @icon-light-gray;
}
}
.@{ns}-test-output {
font-size: 15px;
font-family: "Ubuntu Mono";
margin-top: 8px;
line-height:20px;
word-wrap: break-word;
}
&{ @import "./views/index.less"; }

View File

@ -1,28 +0,0 @@
import React from 'react';
import PureComponent from 'react-pure-render/component';
const mainId = 'fcc-main-frame';
export default class Preview extends PureComponent {
render() {
return (
<div
className='challenges-preview'
>
<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={ mainId }
/>
<div className='spacer' />
</div>
);
}
}
Preview.displayName = 'Preview';

View File

@ -1,5 +1,5 @@
import Show from './components/Show.jsx'; import Show from './Show.jsx';
import ShowMap from './components/map/Map.jsx'; import _Map from './views/map';
export function challengesRoute() { export function challengesRoute() {
return { return {
@ -24,6 +24,6 @@ export function modernChallengesRoute() {
export function mapRoute() { export function mapRoute() {
return { return {
path: 'map', path: 'map',
component: ShowMap component: _Map
}; };
} }

View File

@ -0,0 +1 @@
"challenges"

View File

@ -7,16 +7,17 @@ import {
Row Row
} from 'react-bootstrap'; } from 'react-bootstrap';
import SolutionInput from '../Solution-Input.jsx'; import ChallengeTitle from '../../Challenge-Title.jsx';
import TestSuite from '../Test-Suite.jsx'; import SolutionInput from '../../Solution-Input.jsx';
import Output from '../Output.jsx'; import TestSuite from '../../Test-Suite.jsx';
import Output from '../../Output.jsx';
import { submitChallenge, executeChallenge } from '../../redux/actions.js'; import { submitChallenge, executeChallenge } from '../../redux/actions.js';
import { challengeSelector } from '../../redux/selectors.js'; import { challengeSelector } from '../../redux/selectors.js';
import { descriptionRegex } from '../../utils.js'; import { descriptionRegex } from '../../utils.js';
import { import {
createFormValidator,
isValidURL, isValidURL,
makeRequired, makeRequired
createFormValidator
} from '../../../../utils/form.js'; } from '../../../../utils/form.js';
// provided by redux form // provided by redux form
@ -114,14 +115,14 @@ export class BackEnd extends PureComponent {
'Submit and go to my next challenge' : 'Submit and go to my next challenge' :
"I've completed this challenge"; "I've completed this challenge";
return ( return (
<div> <Row>
<Col <Col
md={ 6 } md={ 6 }
mdOffset={ 3 } mdOffset={ 3 }
xs={ 12 } xs={ 12 }
> >
<Row className='challenge-instructions'> <Row className='challenge-instructions'>
<h3>{ title }</h3> <ChallengeTitle>{ title }</ChallengeTitle>
{ this.renderDescription(description) } { this.renderDescription(description) }
</Row> </Row>
<Row> <Row>
@ -158,7 +159,7 @@ export class BackEnd extends PureComponent {
<TestSuite tests={ tests } /> <TestSuite tests={ tests } />
</Row> </Row>
</Col> </Col>
</div> </Row>
); );
} }
} }

View File

@ -0,0 +1 @@
export { default } from './Back-End.jsx';

View File

@ -3,6 +3,8 @@ import { Button, Modal } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import FontAwesome from 'react-fontawesome'; import FontAwesome from 'react-fontawesome';
import ns from './ns.json';
const propTypes = { const propTypes = {
close: PropTypes.func, close: PropTypes.func,
open: PropTypes.bool.isRequired, open: PropTypes.bool.isRequired,
@ -22,10 +24,10 @@ export default class ClassicModal extends PureComponent {
e.keyCode === 13 && e.keyCode === 13 &&
(e.ctrlKey || e.meta) && (e.ctrlKey || e.meta) &&
open open
) { ) {
e.preventDefault(); e.preventDefault();
submitChallenge(); submitChallenge();
} }
} }
render() { render() {
@ -38,14 +40,14 @@ export default class ClassicModal extends PureComponent {
return ( return (
<Modal <Modal
animation={ false } animation={ false }
dialogClassName='challenge-success-modal' dialogClassName={ `${ns}-success-modal` }
keyboard={ true } keyboard={ true }
onHide={ close } onHide={ close }
onKeyDown={ this.handleKeyDown } onKeyDown={ this.handleKeyDown }
show={ open } show={ open }
> >
<Modal.Header <Modal.Header
className='challenge-list-header' className={ `${ns}-list-header` }
closeButton={ true } closeButton={ true }
> >
<Modal.Title>{ successMessage }</Modal.Title> <Modal.Title>{ successMessage }</Modal.Title>

View File

@ -1,14 +1,14 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Col } from 'react-bootstrap'; import { Row, Col } from 'react-bootstrap';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import Editor from './Editor.jsx'; import Editor from './Editor.jsx';
import SidePanel from './Side-Panel.jsx'; import SidePanel from './Side-Panel.jsx';
import Preview from './Preview.jsx'; import Preview from './Preview.jsx';
import BugModal from '../Bug-Modal.jsx'; import BugModal from '../../Bug-Modal.jsx';
import ClassicModal from '../Classic-Modal.jsx'; import ClassicModal from './Classic-Modal.jsx';
import { challengeSelector } from '../../redux/selectors'; import { challengeSelector } from '../../redux/selectors';
import { import {
executeChallenge, executeChallenge,
@ -116,7 +116,7 @@ export class Challenge extends PureComponent {
} = this.props; } = this.props;
return ( return (
<div> <Row>
<Col <Col
lg={ showPreview ? 3 : 4 } lg={ showPreview ? 3 : 4 }
md={ showPreview ? 3 : 4 } md={ showPreview ? 3 : 4 }
@ -142,7 +142,7 @@ export class Challenge extends PureComponent {
submitChallenge={ submitChallenge } submitChallenge={ submitChallenge }
successMessage={ successMessage } successMessage={ successMessage }
/> />
</div> </Row>
); );
} }
} }

View File

@ -1,21 +1,14 @@
import { Subject } from 'rx'; import { Subject } from 'rx';
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import Codemirror from 'react-codemirror'; import Codemirror from 'react-codemirror';
import NoSSR from 'react-no-ssr'; import NoSSR from 'react-no-ssr';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import MouseTrap from 'mousetrap'; import MouseTrap from 'mousetrap';
import CodeMirrorSkeleton from '../CodeMirrorSkeleton.jsx'; import ns from './ns.json';
import CodeMirrorSkeleton from '../../Code-Mirror-Skeleton.jsx';
const mapStateToProps = createSelector(
state => state.app.windowHeight,
state => state.app.navHeight,
(windowHeight, navHeight) => ({ height: windowHeight - navHeight - 50 })
);
const editorDebounceTimeout = 750; const editorDebounceTimeout = 750;
@ -40,12 +33,11 @@ const defaultProps = {
const propTypes = { const propTypes = {
content: PropTypes.string, content: PropTypes.string,
executeChallenge: PropTypes.func, executeChallenge: PropTypes.func,
height: PropTypes.number,
mode: PropTypes.string, mode: PropTypes.string,
updateFile: PropTypes.func updateFile: PropTypes.func
}; };
export class Editor extends PureComponent { export default class Editor extends PureComponent {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this._editorContent$ = new Subject(); this._editorContent$ = new Subject();
@ -125,16 +117,13 @@ export class Editor extends PureComponent {
} }
render() { render() {
const { executeChallenge, content, height, mode } = this.props; const {
const style = {}; content,
if (height) { executeChallenge,
style.height = height + 'px'; mode
} } = this.props;
return ( return (
<div <div className={ `${ns}-editor` }>
className='challenges-editor'
style={ style }
>
<NoSSR onSSR={ <CodeMirrorSkeleton content={ content } /> }> <NoSSR onSSR={ <CodeMirrorSkeleton content={ content } /> }>
<Codemirror <Codemirror
onChange={ this.handleChange } onChange={ this.handleChange }
@ -151,6 +140,3 @@ export class Editor extends PureComponent {
Editor.defaultProps = defaultProps; Editor.defaultProps = defaultProps;
Editor.displayName = 'Editor'; Editor.displayName = 'Editor';
Editor.propTypes = propTypes; Editor.propTypes = propTypes;
export default connect(mapStateToProps)(Editor);

View File

@ -0,0 +1,20 @@
import React, { PureComponent } from 'react';
import ns from './ns.json';
const mainId = 'fcc-main-frame';
export default class Preview extends PureComponent {
render() {
return (
<div className={ `${ns}-preview` }>
<iframe
className={ `${ns}-preview-frame` }
id={ mainId }
/>
</div>
);
}
}
Preview.displayName = 'Preview';

View File

@ -5,8 +5,11 @@ import { connect } from 'react-redux';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import { Col, Row } from 'react-bootstrap'; import { Col, Row } from 'react-bootstrap';
import TestSuite from '../Test-Suite.jsx'; import ns from './ns.json';
import Output from '../Output.jsx';
import ChallengeTitle from '../../Challenge-Title.jsx';
import TestSuite from '../../Test-Suite.jsx';
import Output from '../../Output.jsx';
import ToolPanel from './Tool-Panel.jsx'; import ToolPanel from './Tool-Panel.jsx';
import { challengeSelector } from '../../redux/selectors'; import { challengeSelector } from '../../redux/selectors';
import { import {
@ -27,8 +30,6 @@ const mapDispatchToProps = {
}; };
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
challengeSelector, challengeSelector,
state => state.app.windowHeight,
state => state.app.navHeight,
state => state.challengesApp.tests, state => state.challengesApp.tests,
state => state.challengesApp.output, state => state.challengesApp.output,
state => state.challengesApp.hintIndex, state => state.challengesApp.hintIndex,
@ -42,8 +43,6 @@ const mapStateToProps = createSelector(
} = {}, } = {},
title title
}, },
windowHeight,
navHeight,
tests, tests,
output, output,
hintIndex, hintIndex,
@ -52,7 +51,6 @@ const mapStateToProps = createSelector(
) => ({ ) => ({
title, title,
description, description,
height: windowHeight - navHeight - 20,
tests, tests,
output, output,
hint: hints[hintIndex], hint: hints[hintIndex],
@ -63,7 +61,6 @@ const mapStateToProps = createSelector(
const propTypes = { const propTypes = {
description: PropTypes.arrayOf(PropTypes.string), description: PropTypes.arrayOf(PropTypes.string),
executeChallenge: PropTypes.func, executeChallenge: PropTypes.func,
height: PropTypes.number,
helpChatRoom: PropTypes.string, helpChatRoom: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
isCodeLocked: PropTypes.bool, isCodeLocked: PropTypes.bool,
@ -108,7 +105,6 @@ export class SidePanel extends PureComponent {
const { const {
title, title,
description, description,
height,
tests = [], tests = [],
output, output,
hint, hint,
@ -120,24 +116,18 @@ export class SidePanel extends PureComponent {
isCodeLocked, isCodeLocked,
unlockUntrustedCode unlockUntrustedCode
} = this.props; } = this.props;
const style = {};
if (height) {
style.height = height + 'px';
}
return ( return (
<div <div
className='challenges-instructions-panel' className={ `${ns}-instructions-panel` }
ref='panel' ref='panel'
style={ style }
> >
<div> <div>
<h4 className='text-center challenge-instructions-title'> <ChallengeTitle>
{ title || 'Happy Coding!' } { title }
</h4> </ChallengeTitle>
<hr />
<Row> <Row>
<Col <Col
className='challenge-instructions' className={ `${ns}-instructions` }
xs={ 12 } xs={ 12 }
> >
{ this.renderDescription(description) } { this.renderDescription(description) }

View File

@ -0,0 +1,109 @@
// should match filename and ./ns.json
@ns: classic;
// make the height no larger than (window - navbar)
.max-element-height(up-to) {
max-height: e(%('calc(100vh - %s)', @navbar-total-height));
overflow-x: hidden;
overflow-y: scroll;
}
.max-element-height(always) {
height: e(%('calc(100vh - %s)', @navbar-total-height));
overflow-x: hidden;
overflow-y: scroll;
}
.@{ns}-instructions-panel {
.max-element-height(always);
padding-bottom: 10px;
padding-left: 5px;
padding-right: 5px;
}
.@{ns}-instructions {
margin-bottom: 5px;
h4 {
margin-bottom: 0;
}
blockquote {
font-size: 90%;
font-family: @font-family-monospace;
color: @code-color;
background-color: #fffbe5;
border-radius: @border-radius-base;
border: 1px solid @pre-border-color;
white-space: pre;
padding: 5px 10px;
margin-bottom: 10px;
margin-top: -5px;
overflow: auto;
}
dfn {
font-family: @font-family-monospace;
color: @code-color;
background-color: @code-bg;
border-radius: @border-radius-base;
padding: 1px 5px;
}
& a, #MDN-links a {
color: #31708f;
}
& a::after, #MDN-links a::after {
font-size: 70%;
font-family: FontAwesome;
content: " \f08e";
}
ol {
font-size: 16px;
}
}
.night {
.@{ns}-instructions blockquote {
background-color: #242424;
border-color: #515151;
color: #ABABAB
}
.@{ns}-instructions dfn {
background-color: #242424;
color: #02a902;
}
.@{ns}-editor .CodeMirror {
background-color:#242424;
color:#ABABAB;
&-gutters {
background-color:#242424;
color:#ABABAB;
}
.cm-bracket, .cm-tag {
color:#5CAFD6;
}
.cm-property, .cm-string {
color:#B5753A;
}
.cm-keyword, .cm-attribute {
color:#9BBBDC;
}
}
}
.@{ns}-editor {
.max-element-height(always);
width: 100%;
}
.@{ns}-preview {
.max-element-height(always);
width: 100%;
}
.@{ns}-preview-frame {
border: 1px solid gray;
border-radius: 5px;
color: @gray-lighter;
height: 99%;
overflow: hidden;
width: 100%;
}

View File

@ -0,0 +1 @@
export default from './Classic.jsx';

View File

@ -0,0 +1 @@
"classic"

View File

@ -0,0 +1,2 @@
&{ @import "./classic/classic.less"; }
&{ @import "./step/step.less"; }

View File

@ -79,10 +79,7 @@ export class Header extends PureComponent {
'Hide all challenges'; 'Hide all challenges';
return ( return (
<div> <div>
<div <div className='text-center'>
className='text-center map-fixed-header'
style={{ top: '50px' }}
>
<p>Challenges required for certifications are marked with a *</p> <p>Challenges required for certifications are marked with a *</p>
<Row className='map-buttons'> <Row className='map-buttons'>
<Button <Button

View File

@ -2,25 +2,18 @@ import React, { PropTypes } from 'react';
import { compose } from 'redux'; import { compose } from 'redux';
import { contain } from 'redux-epic'; import { contain } from 'redux-epic';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import { Col } from 'react-bootstrap'; import { Col, Row } from 'react-bootstrap';
import MapHeader from './Header.jsx'; import MapHeader from './Header.jsx';
import SuperBlock from './Super-Block.jsx'; import SuperBlock from './Super-Block.jsx';
import { fetchChallenges } from '../../redux/actions'; import { fetchChallenges } from '../../redux/actions';
import { updateTitle } from '../../../../redux/actions'; import { updateTitle } from '../../../../redux/actions';
const bindableActions = { fetchChallenges, updateTitle }; const mapStateToProps = state => ({
const mapStateToProps = createSelector( superBlocks: state.challengesApp.superBlocks
state => state.app.windowHeight, });
state => state.app.navHeight, const mapDispatchToProps = { fetchChallenges, updateTitle };
state => state.challengesApp.superBlocks,
(windowHeight, navHeight, superBlocks) => ({
superBlocks,
height: windowHeight - navHeight - 150
})
);
const fetchOptions = { const fetchOptions = {
fetchAction: 'fetchChallenges', fetchAction: 'fetchChallenges',
isPrimed({ superBlocks }) { isPrimed({ superBlocks }) {
@ -29,7 +22,6 @@ const fetchOptions = {
}; };
const propTypes = { const propTypes = {
fetchChallenges: PropTypes.func.isRequired, fetchChallenges: PropTypes.func.isRequired,
height: PropTypes.number,
params: PropTypes.object, params: PropTypes.object,
superBlocks: PropTypes.array, superBlocks: PropTypes.array,
updateTitle: PropTypes.func.isRequired updateTitle: PropTypes.func.isRequired
@ -61,21 +53,16 @@ export class ShowMap extends PureComponent {
render() { render() {
const { superBlocks } = this.props; const { superBlocks } = this.props;
let height = 'auto';
if (!this.props.params) {
height = this.props.height + 'px';
}
return ( return (
<Col xs={ 12 }> <Row>
<MapHeader /> <Col xs={ 12 }>
<div <MapHeader />
className='map-accordion center-block' <div className='map-accordion center-block'>
style={{ height: height }} { this.renderSuperBlocks(superBlocks) }
> <div className='spacer' />
{ this.renderSuperBlocks(superBlocks) } </div>
<div className='spacer' /> </Col>
</div> </Row>
</Col>
); );
} }
} }
@ -84,6 +71,6 @@ ShowMap.displayName = 'Map';
ShowMap.propTypes = propTypes; ShowMap.propTypes = propTypes;
export default compose( export default compose(
connect(mapStateToProps, bindableActions), connect(mapStateToProps, mapDispatchToProps),
contain(fetchOptions) contain(fetchOptions)
)(ShowMap); )(ShowMap);

View File

@ -0,0 +1 @@
export default from './Map.jsx';

View File

@ -6,7 +6,7 @@ import {
FormControl FormControl
} from 'react-bootstrap'; } from 'react-bootstrap';
import SolutionInput from '../Solution-Input.jsx'; import SolutionInput from '../../Solution-Input.jsx';
import { import {
isValidURL, isValidURL,
makeRequired, makeRequired,

View File

@ -3,11 +3,11 @@ import { createSelector } from 'reselect';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Youtube from 'react-youtube'; import Youtube from 'react-youtube';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import { Col } from 'react-bootstrap'; import { Col, Row } from 'react-bootstrap';
import SidePanel from './Side-Panel.jsx'; import SidePanel from './Side-Panel.jsx';
import ToolPanel from './Tool-Panel.jsx'; import ToolPanel from './Tool-Panel.jsx';
import BugModal from '../Bug-Modal.jsx'; import BugModal from '../../Bug-Modal.jsx';
import { challengeSelector } from '../../redux/selectors'; import { challengeSelector } from '../../redux/selectors';
@ -47,7 +47,7 @@ export class Project extends PureComponent {
description description
} = this.props; } = this.props;
return ( return (
<div> <Row>
<Col md={ 4 }> <Col md={ 4 }>
<SidePanel <SidePanel
description={ description } description={ description }
@ -59,19 +59,16 @@ export class Project extends PureComponent {
md={ 8 } md={ 8 }
xs={ 12 } xs={ 12 }
> >
<div className='embed-responsive embed-responsive-16by9'> <Youtube
<Youtube id={ id }
className='embed-responsive-item' videoId={ videoId }
id={ id } />
videoId={ videoId }
/>
</div>
<br /> <br />
<ToolPanel /> <ToolPanel />
<br /> <br />
<BugModal /> <BugModal />
</Col> </Col>
</div> </Row>
); );
} }
} }

View File

@ -1,6 +1,5 @@
import React, { PropTypes } from 'react'; import React, { PropTypes, PureComponent } from 'react';
import ChallengeTitle from '../../Challenge-Title.jsx';
import PureComponent from 'react-pure-render/component';
const propTypes = { const propTypes = {
description: PropTypes.arrayOf(PropTypes.string), description: PropTypes.arrayOf(PropTypes.string),
@ -10,18 +9,6 @@ const propTypes = {
}; };
export default class SidePanel extends PureComponent { export default class SidePanel extends PureComponent {
renderIcon(isCompleted) {
if (!isCompleted) {
return null;
}
return (
<i
className='ion-checkmark-circled text-primary'
title='Completed'
/>
);
}
renderDescription(title = '', description = []) { renderDescription(title = '', description = []) {
return description.map((line, index) => ( return description.map((line, index) => (
<li <li
@ -36,11 +23,9 @@ export default class SidePanel extends PureComponent {
const { title, description, isCompleted } = this.props; const { title, description, isCompleted } = this.props;
return ( return (
<div> <div>
<h4 className='text-center challenge-instructions-title'> <ChallengeTitle isCompleted={ isCompleted }>
{ title } { title }
{ this.renderIcon(isCompleted) } </ChallengeTitle>
</h4>
<hr />
<ul> <ul>
{ this.renderDescription(title, description) } { this.renderDescription(title, description) }
</ul> </ul>

View File

@ -0,0 +1 @@
export default from './Project.jsx';

View File

@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import LightBox from 'react-images'; import LightBox from 'react-images';
import ns from './ns.json';
import { import {
closeLightBoxImage, closeLightBoxImage,
completeAction, completeAction,
@ -180,10 +181,7 @@ export class StepChallenge extends PureComponent {
} }
const [imgUrl, imgAlt, info, action] = step; const [imgUrl, imgAlt, info, action] = step;
return ( return (
<div <div key={ imgUrl }>
className=''
key={ imgUrl }
>
<a <a
href={ imgUrl } href={ imgUrl }
onClick={ this.handleLightBoxOpen } onClick={ this.handleLightBoxOpen }
@ -206,17 +204,17 @@ export class StepChallenge extends PureComponent {
xs={ 12 } xs={ 12 }
> >
<p <p
className='challenge-step-description' className={ `${ns}-description` }
dangerouslySetInnerHTML={{ __html: info }} dangerouslySetInnerHTML={{ __html: info }}
/> />
</Col> </Col>
</Row> </Row>
<div className='spacer' /> <div className='spacer' />
<div className='challenge-button-block'> <div className={ `${ns}-button-block` }>
{ this.renderActionButton(action, completeAction) } { this.renderActionButton(action, completeAction) }
{ this.renderBackButton(currentIndex, stepBackward) } { this.renderBackButton(currentIndex, stepBackward) }
<Col <Col
className='challenge-step-counter large-p text-center' className={ `${ns}-counter large-p text-center` }
sm={ 4 } sm={ 4 }
xs={ 12 } xs={ 12 }
> >
@ -260,23 +258,25 @@ export class StepChallenge extends PureComponent {
closeLightBoxImage closeLightBoxImage
} = this.props; } = this.props;
return ( return (
<Col <Row>
md={ 8 } <Col
mdOffset={ 2 } md={ 8 }
> mdOffset={ 2 }
{ this.renderStep(this.props) } >
<div className='hidden'> { this.renderStep(this.props) }
{ this.renderImages(steps) } <div className='hidden'>
</div> { this.renderImages(steps) }
<LightBox </div>
backdropClosesModal={ true } <LightBox
images={ [ { src: step[0] } ] } backdropClosesModal={ true }
isOpen={ isLightBoxOpen } images={ [ { src: step[0] } ] }
onClose={ closeLightBoxImage } isOpen={ isLightBoxOpen }
showImageCount={ false } onClose={ closeLightBoxImage }
/> showImageCount={ false }
<div className='spacer' /> />
</Col> <div className='spacer' />
</Col>
</Row>
); );
} }
} }

View File

@ -0,0 +1 @@
export default from './Step.jsx';

View File

@ -0,0 +1 @@
"step"

View File

@ -0,0 +1,54 @@
// should match ./ns.json value and filename
@ns: step;
.@{ns}-description {
font-size: 1.5em;
}
.@{ns}-counter {
font-size: 20px;
line-height: 44px;
}
.@{ns}-forward-leave {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 1;
transform: translate(0, 0);
}
.@{ns}-forward-leave-active {
opacity: 0;
transform: translate(-100%, 0);
}
.@{ns}-forward-enter {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 0;
transform: translate(100%, 0);
}
.@{ns}-forward-enter-active {
opacity: 1;
transform: translate(0, 0);
}
.@{ns}-backward-leave {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 1;
transform: translate(0, 0);
}
.@{ns}-backward-leave-active {
opacity: 0;
transform: translate(100%, 0);
}
.@{ns}-backward-enter {
transition: opacity .4s ease-in, transform .3s ease-in-out;
opacity: 0;
transform: translate(-100%, 0);
}
.@{ns}-backward-enter-active {
opacity: 1;
transform: translate(0, 0);
}

View File

@ -70,15 +70,12 @@ export class Lecture extends React.Component {
return ( return (
<Col xs={ 12 }> <Col xs={ 12 }>
<Row> <Row>
<div className='embed-responsive embed-responsive-16by9'> <Youtube
<Youtube id={ id }
className='embed-responsive-item' onError={ this.handleError }
id={ id } opts={ embedOpts }
onError={ this.handleError } videoId={ videoId }
opts={ embedOpts } />
videoId={ videoId }
/>
</div>
</Row> </Row>
<Row> <Row>
<Col <Col

View File

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Col } from 'react-bootstrap'; import { Col, Row } from 'react-bootstrap';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import Lecture from './Lecture.jsx'; import Lecture from './Lecture.jsx';
@ -55,19 +55,21 @@ export class Video extends React.Component {
shouldShowQuestions shouldShowQuestions
} = this.props; } = this.props;
return ( return (
<Col xs={ 12 }> <Row>
<header className='text-center'> <Col xs={ 12 }>
<h4>{ title }</h4> <header className='text-center'>
</header> <h4>{ title }</h4>
<hr /> </header>
<div className='spacer' /> <hr />
<section <div className='spacer' />
className={ 'text-center' } <section
title={ title } className={ 'text-center' }
> title={ title }
{ this.renderBody(shouldShowQuestions) } >
</section> { this.renderBody(shouldShowQuestions) }
</Col> </section>
</Col>
</Row>
); );
} }
} }

View File

@ -0,0 +1 @@
export default from './Video.jsx';

View File

@ -0,0 +1 @@
&{ @import "./challenges/challenges.less"; }

1
common/index.less Normal file
View File

@ -0,0 +1 @@
&{ @import "./app/index.less"; }

View File

@ -50,7 +50,6 @@ var Rx = require('rx'),
Rx.config.longStackSupport = true; Rx.config.longStackSupport = true;
var sync = browserSync.create('fcc-sync-server'); var sync = browserSync.create('fcc-sync-server');
var reload = sync.reload.bind(sync);
// user definable // user definable
var __DEV__ = !yargs.argv.p; var __DEV__ = !yargs.argv.p;
@ -111,7 +110,6 @@ var paths = {
require.resolve('cal-heatmap'), require.resolve('cal-heatmap'),
require.resolve('moment').replace('.js', '.min.js'), require.resolve('moment').replace('.js', '.min.js'),
require.resolve('moment-timezone').replace('index.js', 'builds/moment-timezone-with-data.min.js'), require.resolve('moment-timezone').replace('index.js', 'builds/moment-timezone-with-data.min.js'),
require.resolve('mousetrap').replace('.js', '.min.js'), require.resolve('mousetrap').replace('.js', '.min.js'),
require.resolve('lightbox2').replace('.js', '.min.js'), require.resolve('lightbox2').replace('.js', '.min.js'),
require.resolve('rx').replace('index.js', 'dist/rx.all.min.js') require.resolve('rx').replace('index.js', 'dist/rx.all.min.js')
@ -124,7 +122,10 @@ var paths = {
], ],
less: './client/less/main.less', less: './client/less/main.less',
lessFiles: './client/less/**/*.less', lessFiles: [
'./client/**/*.less',
'./common/**/*.less'
],
manifest: 'server/manifests/', manifest: 'server/manifests/',
@ -304,7 +305,10 @@ gulp.task('less', function() {
.pipe(__DEV__ ? sourcemaps.init() : gutil.noop()) .pipe(__DEV__ ? sourcemaps.init() : gutil.noop())
// compile // compile
.pipe(less({ .pipe(less({
paths: [ path.join(__dirname, 'less', 'includes') ] paths: [
path.join(__dirname, 'client', 'less'),
path.join(__dirname, 'common')
]
})) }))
.pipe(__DEV__ ? .pipe(__DEV__ ?
sourcemaps.write({ sourceRoot: '/less' }) : sourcemaps.write({ sourceRoot: '/less' }) :

View File

@ -6,7 +6,7 @@ html(lang='en')
else else
title freeCodeCamp title freeCodeCamp
include partials/react-stylesheets include partials/react-stylesheets
body.container.react-layout(style='overflow: hidden') body
#fcc!= markup #fcc!= markup
script!= state script!= state
script. script.

View File

@ -3,7 +3,7 @@ html(lang='en')
head head
include partials/meta include partials/meta
include partials/stylesheets include partials/stylesheets
body.top-and-bottom-margins(class=theme !== 'default' ? theme : '') body.main-container(class=theme !== 'default' ? theme : '')
include partials/scripts include partials/scripts
include partials/navbar include partials/navbar
include partials/flash include partials/flash

View File

@ -1,4 +1,4 @@
nav.navbar.navbar-default.navbar-fixed-top.nav-height nav.navbar.navbar-default.navbar-static-top.nav-height
.navbar-header .navbar-header
button.hamburger.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse') button.hamburger.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse')
.col-xs-12 .col-xs-12