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
// --------------------------------------------------
@empty: ~'';
//== Colors
@ -20,8 +21,8 @@
@brand-warning: #f0ad4e;
@brand-danger: #d9534f;
@icon-grey: #575757;
@icon-light-grey: #888888;
@icon-gray: #575757;
@icon-light-gray: #888888;
//== Scaffolding
//
//## Settings for some of the most global styles.
@ -305,6 +306,10 @@
//** Deprecated `@screen-lg-desktop` as of v3.0.1
@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
@screen-xs-max: (@screen-sm-min - 1);
@screen-sm-max: (@screen-md-min - 1);
@ -345,6 +350,8 @@
//** For `@screen-lg-min` and up.
@container-lg: @container-large-desktop;
@container-xl: (1340px + @grid-gutter-width);
//== Navbar
//

View File

@ -1,8 +1,9 @@
@import "lib/bootstrap/bootstrap";
@import "lib/bootstrap-social/bootstrap-social";
@import "lib/ionicons/ionicons";
@import "lib/animate";
@import "lib/bootstrap/variables";
@import "./lib/bootstrap/bootstrap";
@import "./lib/bootstrap-social/bootstrap-social";
@import "./lib/ionicons/ionicons";
@import "./lib/animate";
@import "./lib/bootstrap/variables";
@import "./flexgrid.less";
html,body,div,span,a,li,td,th {
font-family: 'Lato', sans-serif;
@ -34,13 +35,6 @@ pre.wrappable {
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] {
// /* Double-sized Checkboxes */
// -ms-transform: scale(2); /* IE */
@ -54,28 +48,14 @@ html {
border-color: @brand-primary;
}
body.full-screen-body-background {
.full-screen-body-background {
background-color: @body-bg;
}
body.top-and-bottom-margins {
padding-top: 80px;
margin-bottom: 60px;
}
body.no-top-and-bottom-margins {
.no-top-and-bottom-margins {
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 {
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 {
height: 50px;
border: none;
height: @navbar-height;
width: 100%;
}
.landing-icon {
@ -1241,11 +1224,14 @@ and (max-width : 400px) {
}
}
@import "code-mirror.less";
@import "challenge.less";
@import "toastr.less";
@import "map.less";
@import "sk-wave.less";
@import "classic-modal.less";
@import "skeleton-shimmer.less";
// surrounding downstream import with &{}
// creates locally scoped imports
// and prevents vaiables from overwriting each other
&{ @import "./code-mirror.less"; }
&{ @import "./challenge.less"; }
&{ @import "./toastr.less"; }
&{ @import "./map.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%;
}
.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 {
width: 100%;
margin-top: 100px;
max-width: 700px;
overflow-y: auto;
width: 100%;
.map-accordion-panel-title {
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 nightModeSaga from './night-mode-saga.js';
import titleSaga from './title-saga.js';
import windowSaga from './window-saga.js';
export default [
analyticsSaga,
@ -18,6 +17,5 @@ export default [
hardGoToSaga,
mouseTrapSaga,
nightModeSaga,
titleSaga,
windowSaga
titleSaga
];

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 { Button, Row } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import ns from './ns.json';
import {
fetchUser,
initWindowHeight,
updateNavHeight,
updateAppLang,
trackEvent,
loadCurrentChallenge,
@ -21,15 +20,13 @@ import Toasts from './toasts/Toasts.jsx';
import { userSelector } from './redux/selectors';
const mapDispatchToProps = {
initWindowHeight,
updateNavHeight,
closeDropdown,
fetchUser,
submitChallenge,
updateAppLang,
trackEvent,
loadCurrentChallenge,
openDropdown,
closeDropdown
submitChallenge,
trackEvent,
updateAppLang
};
const mapStateToProps = createSelector(
@ -58,7 +55,6 @@ const propTypes = {
children: PropTypes.node,
closeDropdown: PropTypes.func.isRequired,
fetchUser: PropTypes.func,
initWindowHeight: PropTypes.func,
isNavDropdownOpen: PropTypes.bool,
isSignedIn: PropTypes.bool,
loadCurrentChallenge: PropTypes.func.isRequired,
@ -71,7 +67,6 @@ const propTypes = {
toast: PropTypes.object,
trackEvent: PropTypes.func.isRequired,
updateAppLang: PropTypes.func.isRequired,
updateNavHeight: PropTypes.func,
username: PropTypes.string
};
@ -84,7 +79,6 @@ export class FreeCodeCamp extends React.Component {
}
componentDidMount() {
this.props.initWindowHeight();
if (!this.props.isSignedIn) {
this.props.fetchUser();
}
@ -110,7 +104,6 @@ export class FreeCodeCamp extends React.Component {
username,
points,
picture,
updateNavHeight,
trackEvent,
loadCurrentChallenge,
openDropdown,
@ -118,23 +111,22 @@ export class FreeCodeCamp extends React.Component {
isNavDropdownOpen
} = this.props;
const navProps = {
username,
points,
picture,
updateNavHeight,
trackEvent,
closeDropdown,
isNavDropdownOpen,
loadCurrentChallenge,
openDropdown,
closeDropdown,
isNavDropdownOpen
picture,
points,
trackEvent,
username
};
return (
<div>
<div className={ `${ns}-container` }>
<Nav { ...navProps }/>
<Row>
<div className={ `${ns}-content` }>
{ this.props.children }
</Row>
</div>
<Toasts />
</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 ReactDOM from 'react-dom';
import { LinkContainer } from 'react-router-bootstrap';
import {
Col,
@ -41,7 +40,6 @@ const propTypes = {
showLoading: PropTypes.bool,
signedIn: PropTypes.bool,
trackEvent: PropTypes.func.isRequired,
updateNavHeight: PropTypes.func,
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) {
e.preventDefault();
this.props.trackEvent({
@ -172,7 +165,7 @@ export default class FCCNav extends React.Component {
return (
<Navbar
className='nav-height'
fixedTop={ true }
staticTop={ true }
>
<Navbar.Header>
<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
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
export const updateChallengesData = createAction(types.updateChallengesData);
export const updateHikesData = createAction(types.updateHikesData);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,8 @@ import React, { PropTypes, PureComponent } from 'react';
import classnames from 'classnames';
import { Col, Row } from 'react-bootstrap';
import ns from './ns.json';
const propTypes = {
tests: PropTypes.arrayOf(PropTypes.object)
};
@ -41,7 +43,7 @@ export default class TestSuite extends PureComponent {
const { tests } = this.props;
return (
<div
className='challenge-test-suite'
className={ `${ns}-test-suite` }
style={{ marginTop: '10px' }}
>
{ 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 ShowMap from './components/map/Map.jsx';
import Show from './Show.jsx';
import _Map from './views/map';
export function challengesRoute() {
return {
@ -24,6 +24,6 @@ export function modernChallengesRoute() {
export function mapRoute() {
return {
path: 'map',
component: ShowMap
component: _Map
};
}

View File

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

View File

@ -7,16 +7,17 @@ import {
Row
} from 'react-bootstrap';
import SolutionInput from '../Solution-Input.jsx';
import TestSuite from '../Test-Suite.jsx';
import Output from '../Output.jsx';
import ChallengeTitle from '../../Challenge-Title.jsx';
import SolutionInput from '../../Solution-Input.jsx';
import TestSuite from '../../Test-Suite.jsx';
import Output from '../../Output.jsx';
import { submitChallenge, executeChallenge } from '../../redux/actions.js';
import { challengeSelector } from '../../redux/selectors.js';
import { descriptionRegex } from '../../utils.js';
import {
createFormValidator,
isValidURL,
makeRequired,
createFormValidator
makeRequired
} from '../../../../utils/form.js';
// provided by redux form
@ -114,14 +115,14 @@ export class BackEnd extends PureComponent {
'Submit and go to my next challenge' :
"I've completed this challenge";
return (
<div>
<Row>
<Col
md={ 6 }
mdOffset={ 3 }
xs={ 12 }
>
<Row className='challenge-instructions'>
<h3>{ title }</h3>
<ChallengeTitle>{ title }</ChallengeTitle>
{ this.renderDescription(description) }
</Row>
<Row>
@ -158,7 +159,7 @@ export class BackEnd extends PureComponent {
<TestSuite tests={ tests } />
</Row>
</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 FontAwesome from 'react-fontawesome';
import ns from './ns.json';
const propTypes = {
close: PropTypes.func,
open: PropTypes.bool.isRequired,
@ -22,10 +24,10 @@ export default class ClassicModal extends PureComponent {
e.keyCode === 13 &&
(e.ctrlKey || e.meta) &&
open
) {
e.preventDefault();
submitChallenge();
}
) {
e.preventDefault();
submitChallenge();
}
}
render() {
@ -38,14 +40,14 @@ export default class ClassicModal extends PureComponent {
return (
<Modal
animation={ false }
dialogClassName='challenge-success-modal'
dialogClassName={ `${ns}-success-modal` }
keyboard={ true }
onHide={ close }
onKeyDown={ this.handleKeyDown }
show={ open }
>
<Modal.Header
className='challenge-list-header'
className={ `${ns}-list-header` }
closeButton={ true }
>
<Modal.Title>{ successMessage }</Modal.Title>

View File

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

View File

@ -1,21 +1,14 @@
import { Subject } from 'rx';
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';
import MouseTrap from 'mousetrap';
import CodeMirrorSkeleton from '../CodeMirrorSkeleton.jsx';
const mapStateToProps = createSelector(
state => state.app.windowHeight,
state => state.app.navHeight,
(windowHeight, navHeight) => ({ height: windowHeight - navHeight - 50 })
);
import ns from './ns.json';
import CodeMirrorSkeleton from '../../Code-Mirror-Skeleton.jsx';
const editorDebounceTimeout = 750;
@ -40,12 +33,11 @@ const defaultProps = {
const propTypes = {
content: PropTypes.string,
executeChallenge: PropTypes.func,
height: PropTypes.number,
mode: PropTypes.string,
updateFile: PropTypes.func
};
export class Editor extends PureComponent {
export default class Editor extends PureComponent {
constructor(...args) {
super(...args);
this._editorContent$ = new Subject();
@ -125,16 +117,13 @@ export class Editor extends PureComponent {
}
render() {
const { executeChallenge, content, height, mode } = this.props;
const style = {};
if (height) {
style.height = height + 'px';
}
const {
content,
executeChallenge,
mode
} = this.props;
return (
<div
className='challenges-editor'
style={ style }
>
<div className={ `${ns}-editor` }>
<NoSSR onSSR={ <CodeMirrorSkeleton content={ content } /> }>
<Codemirror
onChange={ this.handleChange }
@ -151,6 +140,3 @@ export class Editor extends PureComponent {
Editor.defaultProps = defaultProps;
Editor.displayName = 'Editor';
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 { Col, Row } from 'react-bootstrap';
import TestSuite from '../Test-Suite.jsx';
import Output from '../Output.jsx';
import ns from './ns.json';
import ChallengeTitle from '../../Challenge-Title.jsx';
import TestSuite from '../../Test-Suite.jsx';
import Output from '../../Output.jsx';
import ToolPanel from './Tool-Panel.jsx';
import { challengeSelector } from '../../redux/selectors';
import {
@ -27,8 +30,6 @@ const mapDispatchToProps = {
};
const mapStateToProps = createSelector(
challengeSelector,
state => state.app.windowHeight,
state => state.app.navHeight,
state => state.challengesApp.tests,
state => state.challengesApp.output,
state => state.challengesApp.hintIndex,
@ -42,8 +43,6 @@ const mapStateToProps = createSelector(
} = {},
title
},
windowHeight,
navHeight,
tests,
output,
hintIndex,
@ -52,7 +51,6 @@ const mapStateToProps = createSelector(
) => ({
title,
description,
height: windowHeight - navHeight - 20,
tests,
output,
hint: hints[hintIndex],
@ -63,7 +61,6 @@ const mapStateToProps = createSelector(
const propTypes = {
description: PropTypes.arrayOf(PropTypes.string),
executeChallenge: PropTypes.func,
height: PropTypes.number,
helpChatRoom: PropTypes.string,
hint: PropTypes.string,
isCodeLocked: PropTypes.bool,
@ -108,7 +105,6 @@ export class SidePanel extends PureComponent {
const {
title,
description,
height,
tests = [],
output,
hint,
@ -120,24 +116,18 @@ export class SidePanel extends PureComponent {
isCodeLocked,
unlockUntrustedCode
} = this.props;
const style = {};
if (height) {
style.height = height + 'px';
}
return (
<div
className='challenges-instructions-panel'
className={ `${ns}-instructions-panel` }
ref='panel'
style={ style }
>
<div>
<h4 className='text-center challenge-instructions-title'>
{ title || 'Happy Coding!' }
</h4>
<hr />
<ChallengeTitle>
{ title }
</ChallengeTitle>
<Row>
<Col
className='challenge-instructions'
className={ `${ns}-instructions` }
xs={ 12 }
>
{ 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';
return (
<div>
<div
className='text-center map-fixed-header'
style={{ top: '50px' }}
>
<div className='text-center'>
<p>Challenges required for certifications are marked with a *</p>
<Row className='map-buttons'>
<Button

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
import React, { PropTypes, PureComponent } from 'react';
import ChallengeTitle from '../../Challenge-Title.jsx';
const propTypes = {
description: PropTypes.arrayOf(PropTypes.string),
@ -10,18 +9,6 @@ const propTypes = {
};
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 = []) {
return description.map((line, index) => (
<li
@ -36,11 +23,9 @@ export default class SidePanel extends PureComponent {
const { title, description, isCompleted } = this.props;
return (
<div>
<h4 className='text-center challenge-instructions-title'>
<ChallengeTitle isCompleted={ isCompleted }>
{ title }
{ this.renderIcon(isCompleted) }
</h4>
<hr />
</ChallengeTitle>
<ul>
{ this.renderDescription(title, description) }
</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 LightBox from 'react-images';
import ns from './ns.json';
import {
closeLightBoxImage,
completeAction,
@ -180,10 +181,7 @@ export class StepChallenge extends PureComponent {
}
const [imgUrl, imgAlt, info, action] = step;
return (
<div
className=''
key={ imgUrl }
>
<div key={ imgUrl }>
<a
href={ imgUrl }
onClick={ this.handleLightBoxOpen }
@ -206,17 +204,17 @@ export class StepChallenge extends PureComponent {
xs={ 12 }
>
<p
className='challenge-step-description'
className={ `${ns}-description` }
dangerouslySetInnerHTML={{ __html: info }}
/>
</Col>
</Row>
<div className='spacer' />
<div className='challenge-button-block'>
<div className={ `${ns}-button-block` }>
{ this.renderActionButton(action, completeAction) }
{ this.renderBackButton(currentIndex, stepBackward) }
<Col
className='challenge-step-counter large-p text-center'
className={ `${ns}-counter large-p text-center` }
sm={ 4 }
xs={ 12 }
>
@ -260,23 +258,25 @@ export class StepChallenge extends PureComponent {
closeLightBoxImage
} = this.props;
return (
<Col
md={ 8 }
mdOffset={ 2 }
>
{ this.renderStep(this.props) }
<div className='hidden'>
{ this.renderImages(steps) }
</div>
<LightBox
backdropClosesModal={ true }
images={ [ { src: step[0] } ] }
isOpen={ isLightBoxOpen }
onClose={ closeLightBoxImage }
showImageCount={ false }
/>
<div className='spacer' />
</Col>
<Row>
<Col
md={ 8 }
mdOffset={ 2 }
>
{ this.renderStep(this.props) }
<div className='hidden'>
{ this.renderImages(steps) }
</div>
<LightBox
backdropClosesModal={ true }
images={ [ { src: step[0] } ] }
isOpen={ isLightBoxOpen }
onClose={ closeLightBoxImage }
showImageCount={ false }
/>
<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 (
<Col xs={ 12 }>
<Row>
<div className='embed-responsive embed-responsive-16by9'>
<Youtube
className='embed-responsive-item'
id={ id }
onError={ this.handleError }
opts={ embedOpts }
videoId={ videoId }
/>
</div>
<Youtube
id={ id }
onError={ this.handleError }
opts={ embedOpts }
videoId={ videoId }
/>
</Row>
<Row>
<Col

View File

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Col } from 'react-bootstrap';
import { Col, Row } from 'react-bootstrap';
import { createSelector } from 'reselect';
import Lecture from './Lecture.jsx';
@ -55,19 +55,21 @@ export class Video extends React.Component {
shouldShowQuestions
} = this.props;
return (
<Col xs={ 12 }>
<header className='text-center'>
<h4>{ title }</h4>
</header>
<hr />
<div className='spacer' />
<section
className={ 'text-center' }
title={ title }
>
{ this.renderBody(shouldShowQuestions) }
</section>
</Col>
<Row>
<Col xs={ 12 }>
<header className='text-center'>
<h4>{ title }</h4>
</header>
<hr />
<div className='spacer' />
<section
className={ 'text-center' }
title={ title }
>
{ this.renderBody(shouldShowQuestions) }
</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;
var sync = browserSync.create('fcc-sync-server');
var reload = sync.reload.bind(sync);
// user definable
var __DEV__ = !yargs.argv.p;
@ -111,7 +110,6 @@ var paths = {
require.resolve('cal-heatmap'),
require.resolve('moment').replace('.js', '.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('lightbox2').replace('.js', '.min.js'),
require.resolve('rx').replace('index.js', 'dist/rx.all.min.js')
@ -124,7 +122,10 @@ var paths = {
],
less: './client/less/main.less',
lessFiles: './client/less/**/*.less',
lessFiles: [
'./client/**/*.less',
'./common/**/*.less'
],
manifest: 'server/manifests/',
@ -304,7 +305,10 @@ gulp.task('less', function() {
.pipe(__DEV__ ? sourcemaps.init() : gutil.noop())
// compile
.pipe(less({
paths: [ path.join(__dirname, 'less', 'includes') ]
paths: [
path.join(__dirname, 'client', 'less'),
path.join(__dirname, 'common')
]
}))
.pipe(__DEV__ ?
sourcemaps.write({ sourceRoot: '/less' }) :

View File

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

View File

@ -3,7 +3,7 @@ html(lang='en')
head
include partials/meta
include partials/stylesheets
body.top-and-bottom-margins(class=theme !== 'default' ? theme : '')
body.main-container(class=theme !== 'default' ? theme : '')
include partials/scripts
include partials/navbar
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
button.hamburger.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse')
.col-xs-12