diff --git a/client/index.js b/client/index.js
index 969dab1366..514fb1a6f0 100644
--- a/client/index.js
+++ b/client/index.js
@@ -4,20 +4,13 @@ import Rx from 'rx';
import debug from 'debug';
import { render } from 'redux-epic';
import createHistory from 'history/createBrowserHistory';
-import useLangRoutes from './utils/use-lang-routes';
import sendPageAnalytics from './utils/send-page-analytics';
import { App, createApp, provideStore } from '../common/app';
-import { getLangFromPath } from '../common/app/utils/lang';
// client specific epics
import epics from './epics';
-import {
- isColdStored,
- getColdStorage,
- saveToColdStorage
-} from './cold-reload';
const {
__OPBEAT__ORG_ID,
@@ -47,7 +40,7 @@ const {
document,
ga,
__fcc__: {
- data: ssrState = {},
+ data: defaultState = {},
csrf: {
token: csrfToken
} = {}
@@ -63,10 +56,6 @@ const epicOptions = {
const DOMContainer = document.getElementById('fcc');
-const defaultState = isColdStored() ?
- getColdStorage() :
- ssrState;
-const primaryLang = getLangFromPath(location.pathname);
defaultState.app.csrfToken = csrfToken;
@@ -76,7 +65,7 @@ const serviceOptions = {
xhrTimeout: 15000
};
-const history = useLangRoutes(createHistory, primaryLang)();
+const history = createHistory();
sendPageAnalytics(history, ga);
createApp({
@@ -88,14 +77,13 @@ createApp({
enhancers: isDev && devToolsExtension && [ devToolsExtension() ],
middlewares: enableOpbeat && [ createOpbeatMiddleware() ]
})
- .doOnNext(({ store }) => {
+ .doOnNext(() => {
if (module.hot && typeof module.hot.accept === 'function') {
module.hot.accept(() => {
// note(berks): not sure this ever runs anymore after adding
// RHR?
log('saving state and refreshing.');
log('ignore react ssr warning.');
- saveToColdStorage(store.getState());
setTimeout(() => location.reload(), hotReloadTimeout);
});
}
diff --git a/client/less/main.less b/client/less/main.less
index f1653e0759..db9a804e05 100644
--- a/client/less/main.less
+++ b/client/less/main.less
@@ -152,6 +152,24 @@ h1, h2, h3, h4, h5, h6, p, li {
width: 100px;
}
+p.stats {
+ font-size: 2rem;
+}
+
+.green-text {
+ color: #006400;
+}
+
+.more-button-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ div {
+ flex-grow: 1;
+ }
+}
+
.completion-icon {
font-size: 150px;
}
diff --git a/client/utils/use-lang-routes.js b/client/utils/use-lang-routes.js
deleted file mode 100644
index d254e1ac09..0000000000
--- a/client/utils/use-lang-routes.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { addLang, getLangFromPath } from '../../common/app/utils/lang.js';
-
-function addLangToLocation(location, lang) {
- if (!location) {
- return location;
- }
- if (typeof location === 'string') {
- return addLang(location, lang);
- }
- return {
- ...location,
- pathname: addLang(location.pathname, lang)
- };
-}
-
-function getLangFromLocation(location) {
- if (!location) {
- return location;
- }
- if (typeof location === 'string') {
- return getLangFromPath(location);
- }
- return getLangFromPath(location.pathname);
-}
-
-export default function useLangRoutes(createHistory, primaryLang) {
- return (options = {}) => {
- let lang = primaryLang || 'en';
- const history = createHistory(options);
- const unsubscribeFromHistory = history.listen(nextLocation => {
- lang = getLangFromLocation(nextLocation);
- });
- const push = location => history.push(addLangToLocation(location, lang));
- const replace = location => history.replace(
- addLangToLocation(location, lang)
- );
- return {
- ...history,
- push,
- replace,
- unsubscribe() { unsubscribeFromHistory(); }
- };
- };
-}
diff --git a/common/app/App.jsx b/common/app/App.jsx
index f4e17b61f6..7d32cdc6e6 100644
--- a/common/app/App.jsx
+++ b/common/app/App.jsx
@@ -10,17 +10,19 @@ import {
isSignedInSelector
} from './redux';
+import { fetchMapUi } from './Map/redux';
+
import Flash from './Flash';
import Nav from './Nav';
import Toasts from './Toasts';
import NotFound from './NotFound';
import { mainRouteSelector } from './routes/redux';
-import Challenges from './routes/Challenges';
import Profile from './routes/Profile';
import Settings from './routes/Settings';
const mapDispatchToProps = {
appMounted,
+ fetchMapUi,
fetchUser
};
@@ -37,6 +39,7 @@ const mapStateToProps = state => {
const propTypes = {
appMounted: PropTypes.func.isRequired,
children: PropTypes.node,
+ fetchMapUi: PropTypes.func.isRequired,
fetchUser: PropTypes.func,
isSignedIn: PropTypes.bool,
route: PropTypes.string,
@@ -44,7 +47,6 @@ const propTypes = {
};
const routes = {
- challenges: Challenges,
profile: Profile,
settings: Settings
};
@@ -53,6 +55,7 @@ const routes = {
export class FreeCodeCamp extends React.Component {
componentDidMount() {
this.props.appMounted();
+ this.props.fetchMapUi();
if (!this.props.isSignedIn) {
this.props.fetchUser();
}
diff --git a/common/app/Map/Block.jsx b/common/app/Map/Block.jsx
deleted file mode 100644
index 1387faa0bd..0000000000
--- a/common/app/Map/Block.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import FA from 'react-fontawesome';
-import { Panel } from 'react-bootstrap';
-
-import ns from './ns.json';
-import Challenges from './Challenges.jsx';
-import {
- toggleThisPanel,
- makePanelOpenSelector
-} from './redux';
-import { fetchNewBlock } from '../redux';
-
-import { makeBlockSelector } from '../entities';
-
-const mapDispatchToProps = {
- fetchNewBlock,
- toggleThisPanel
-};
-function makeMapStateToProps(_, { dashedName }) {
- return createSelector(
- makeBlockSelector(dashedName),
- makePanelOpenSelector(dashedName),
- (block, isOpen) => {
- return {
- isOpen,
- dashedName,
- title: block.title,
- time: block.time,
- challenges: block.challenges || []
- };
- }
- );
-}
-const propTypes = {
- challenges: PropTypes.array,
- dashedName: PropTypes.string,
- fetchNewBlock: PropTypes.func.isRequired,
- isOpen: PropTypes.bool,
- time: PropTypes.string,
- title: PropTypes.string,
- toggleThisPanel: PropTypes.func
-};
-
-export class Block extends PureComponent {
- constructor(...props) {
- super(...props);
- this.handleSelect = this.handleSelect.bind(this);
- }
- handleSelect(eventKey, e) {
- e.preventDefault();
- this.props.toggleThisPanel(eventKey);
- }
-
- renderHeader(isOpen, title, time, isCompleted) {
- return (
-
-
-
- { title }
-
- {
- time && ({ time })
- }
-
- );
- }
-
- render() {
- const {
- title,
- time,
- dashedName,
- isOpen,
- challenges,
- fetchNewBlock
- } = this.props;
- return (
- fetchNewBlock(dashedName) }
- onSelect={ this.handleSelect }
- >
- { isOpen && }
-
- );
- }
-}
-
-Block.displayName = 'Block';
-Block.propTypes = propTypes;
-
-export default connect(makeMapStateToProps, mapDispatchToProps)(Block);
diff --git a/common/app/Map/Blocks.jsx b/common/app/Map/Blocks.jsx
deleted file mode 100644
index 879a928334..0000000000
--- a/common/app/Map/Blocks.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import ns from './ns.json';
-import Block from './Block.jsx';
-
-const propTypes = {
- blocks: PropTypes.array.isRequired
-};
-
-export default class Blocks extends Component {
- shouldComponentUpdate(nextProps) {
- return this.props.blocks !== nextProps.blocks;
- }
-
- render() {
- const {
- blocks
- } = this.props;
- if (blocks.length <= 0) {
- return null;
- }
- return (
-
- {
- blocks.map(dashedName => (
-
- ))
- }
-
- );
- }
-}
-
-Blocks.displayName = 'Blocks';
-Blocks.propTypes = propTypes;
diff --git a/common/app/Map/Challenge.jsx b/common/app/Map/Challenge.jsx
deleted file mode 100644
index ccc4d18401..0000000000
--- a/common/app/Map/Challenge.jsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import classnames from 'classnames';
-import debug from 'debug';
-
-import { clickOnChallenge } from './redux';
-import { userSelector } from '../redux';
-import { paramsSelector } from '../Router/redux';
-import { challengeMapSelector } from '../entities';
-import { Link } from '../Router';
-import { onRouteChallenges } from '../routes/Challenges/redux';
-
-const propTypes = {
- block: PropTypes.string,
- challenge: PropTypes.object,
- clickOnChallenge: PropTypes.func.isRequired,
- dashedName: PropTypes.string,
- isComingSoon: PropTypes.bool,
- isCompleted: PropTypes.bool,
- isDev: PropTypes.bool,
- isLocked: PropTypes.bool,
- selected: PropTypes.bool,
- title: PropTypes.string
-};
-const mapDispatchToProps = { clickOnChallenge };
-
-function makeMapStateToProps(_, { dashedName }) {
- return createSelector(
- userSelector,
- challengeMapSelector,
- paramsSelector,
- (
- { challengeMap: userChallengeMap },
- challengeMap,
- params
- ) => {
- const {
- id,
- title,
- block,
- isLocked,
- isComingSoon
- } = challengeMap[dashedName] || {};
- const isCompleted = userChallengeMap ? !!userChallengeMap[id] : false;
- const selected = dashedName === params.dashedName;
- return {
- dashedName,
- isCompleted,
- title,
- block,
- isLocked,
- isComingSoon,
- isDev: debug.enabled('fcc:*'),
- selected
- };
- }
- );
-}
-
-export class Challenge extends PureComponent {
- renderCompleted(isCompleted, isLocked) {
- if (isLocked || !isCompleted) {
- return null;
- }
- return completed;
- }
-
- renderComingSoon(isComingSoon) {
- if (!isComingSoon) {
- return null;
- }
- return (
-
-
-
- Coming Soon
-
-
- );
- }
-
- renderLocked(title, isComingSoon, className) {
- return (
-
- { title }
- { this.renderComingSoon(isComingSoon) }
-
- );
- }
-
-
- render() {
- const {
- block,
- clickOnChallenge,
- dashedName,
- isComingSoon,
- isCompleted,
- isDev,
- isLocked,
- title,
- selected
- } = this.props;
- if (!title) {
- return null;
- }
- const challengeClassName = classnames({
- 'text-primary': true,
- 'padded-ionic-icon': true,
- 'map-challenge-title': true,
- 'ion-checkmark-circled faded': !(isLocked || isComingSoon) && isCompleted,
- 'ion-ios-circle-outline': !(isLocked || isComingSoon) && !isCompleted,
- 'ion-locked': isLocked || isComingSoon,
- disabled: isLocked || (!isDev && isComingSoon),
- selectedChallenge: selected
- });
- if (isLocked || (!isDev && isComingSoon)) {
- return this.renderLocked(
- title,
- isComingSoon,
- challengeClassName
- );
- }
- return (
-
-
-
- { title }
- { this.renderCompleted(isCompleted, isLocked) }
-
-
-
- );
- }
-}
-
-Challenge.propTypes = propTypes;
-Challenge.dispalyName = 'Challenge';
-
-export default connect(
- makeMapStateToProps,
- mapDispatchToProps
-)(Challenge);
diff --git a/common/app/Map/Challenges.jsx b/common/app/Map/Challenges.jsx
deleted file mode 100644
index 975553a48a..0000000000
--- a/common/app/Map/Challenges.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import Challenge from './Challenge.jsx';
-
-const propTypes = {
- challenges: PropTypes.array.isRequired
-};
-
-export default class Challenges extends Component {
- shouldComponentUpdate(nextProps) {
- return this.props.challenges !== nextProps.challenges;
- }
- render() {
- const { challenges } = this.props;
- if (!challenges.length) {
- return No Challenges Found
;
- }
- return (
-
- {
- challenges.map(dashedName => (
-
- ))
- }
-
- );
- }
-}
-
-Challenges.displayName = 'Challenges';
-Challenges.propTypes = propTypes;
diff --git a/common/app/Map/Map.jsx b/common/app/Map/Map.jsx
deleted file mode 100644
index 6d9ec9e3c8..0000000000
--- a/common/app/Map/Map.jsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { Col, Row } from 'react-bootstrap';
-
-import ns from './ns.json';
-import { Loader } from '../helperComponents';
-import SuperBlock from './Super-Block.jsx';
-import { currentChallengeSelector, superBlocksSelector } from '../redux';
-import { fetchMapUi } from './redux';
-
-const mapStateToProps = state => ({
- currentChallenge: currentChallengeSelector(state),
- superBlocks: superBlocksSelector(state)
-});
-
-const mapDispatchToProps = { fetchMapUi };
-const propTypes = {
- currentChallenge: PropTypes.string,
- fetchMapUi: PropTypes.func.isRequired,
- params: PropTypes.object,
- superBlocks: PropTypes.array
-};
-
-export class ShowMap extends PureComponent {
- componentDidMount() {
- this.setupMapScroll();
- }
-
- componentDidUpdate() {
- this.setupMapScroll();
- }
-
- setupMapScroll() {
- this.updateMapScrollAttempts = 0;
- this.updateMapScroll();
- }
-
- updateMapScroll() {
- const { currentChallenge } = this.props;
- const rowNode = this._row;
- const challengeNode = rowNode.querySelector(
- `[data-challenge="${currentChallenge}"]`
- );
-
- if ( !challengeNode ) {
- this.retryUpdateMapScroll();
- return;
- }
-
- const containerScrollHeight = rowNode.scrollHeight;
- const containerHeight = rowNode.clientHeight;
-
- const offset = 100;
- const itemTop = challengeNode.offsetTop;
- const itemBottom = itemTop + challengeNode.clientHeight;
-
- const currentViewBottom = rowNode.scrollTop + containerHeight;
-
- if ( itemBottom + offset < currentViewBottom ) {
- // item is visible with enough offset from bottom => no need to scroll
- return;
- }
-
- if ( containerHeight === containerScrollHeight ) {
- /*
- * During a first run containerNode scrollHeight may be not updated yet.
- * In this case containerNode ignores changes of scrollTop property.
- * So we have to wait some time before scrollTop can be updated
- * */
- this.retryUpdateMapScroll();
- return;
- }
-
- const scrollTop = itemBottom + offset - containerHeight;
- rowNode.scrollTop = scrollTop;
- }
-
- retryUpdateMapScroll() {
- const maxAttempts = 5;
- this.updateMapScrollAttempts++;
-
- if (this.updateMapScrollAttempts < maxAttempts) {
- setTimeout(() => this.updateMapScroll(), 300);
- }
- }
-
- renderSuperBlocks() {
- const { superBlocks } = this.props;
- if (!Array.isArray(superBlocks) || !superBlocks.length) {
- return (
-
-
-
- );
- }
- return superBlocks.map(dashedName => (
-
- ));
- }
-
- render() {
- return (
- { this._row = ref; }}>
-
-
-
- { this.renderSuperBlocks() }
-
-
-
-
-
- );
- }
-}
-
-ShowMap.displayName = 'Map';
-ShowMap.propTypes = propTypes;
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ShowMap);
diff --git a/common/app/Map/Super-Block.jsx b/common/app/Map/Super-Block.jsx
deleted file mode 100644
index c5ce006263..0000000000
--- a/common/app/Map/Super-Block.jsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import FA from 'react-fontawesome';
-import { Panel } from 'react-bootstrap';
-
-import ns from './ns.json';
-import Blocks from './Blocks.jsx';
-import {
- toggleThisPanel,
-
- makePanelOpenSelector
-} from './redux';
-import { makeSuperBlockSelector } from '../entities';
-
-const mapDispatchToProps = { toggleThisPanel };
-// make selectors unique to each component
-// see
-// reactjs/reselect
-// sharing-selectors-with-props-across-multiple-components
-function mapStateToProps(_, { dashedName }) {
- return createSelector(
- makeSuperBlockSelector(dashedName),
- makePanelOpenSelector(dashedName),
- (superBlock, isOpen) => ({
- isOpen,
- dashedName,
- title: superBlock.title || dashedName,
- blocks: superBlock.blocks || []
- })
- );
-}
-
-const propTypes = {
- blocks: PropTypes.array,
- dashedName: PropTypes.string,
- isOpen: PropTypes.bool,
- title: PropTypes.string,
- toggleThisPanel: PropTypes.func
-};
-
-export class SuperBlock extends PureComponent {
- constructor(...props) {
- super(...props);
- this.handleSelect = this.handleSelect.bind(this);
- }
- handleSelect(eventKey, e) {
- e.preventDefault();
- this.props.toggleThisPanel(eventKey);
- }
-
- renderHeader(isOpen, title, isCompleted) {
- return (
-
-
- { title }
-
- );
- }
-
- render() {
- const {
- title,
- dashedName,
- blocks,
- isOpen
- } = this.props;
- return (
-
-
-
- );
- }
-}
-
-SuperBlock.displayName = 'SuperBlock';
-SuperBlock.propTypes = propTypes;
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(SuperBlock);
diff --git a/common/app/Map/index.js b/common/app/Map/index.js
deleted file mode 100644
index a046dd5d2d..0000000000
--- a/common/app/Map/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default from './Map.jsx';
diff --git a/common/app/Map/map.less b/common/app/Map/map.less
deleted file mode 100644
index c95767bf79..0000000000
--- a/common/app/Map/map.less
+++ /dev/null
@@ -1,144 +0,0 @@
-// should be the same as the filename and ./ns.json
-@ns: map;
-
-.@{ns}-container {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- overflow-x: hidden;
- overflow-y: auto;
-}
-
-.@{ns}-accordion {
- max-width: 700px;
- overflow-y: auto;
- width: 100%;
-
- a:focus {
- text-decoration: none;
- color:darkgreen;
- }
-
- a:focus:hover {
- text-decoration: underline;
- color:#001800;
- }
-
- @media (max-width: 720px) {
- left: 0;
- right: 0;
- width: 100%;
- top: 195px;
- bottom: 0;
- // position:absolute;
- overflow-x: hidden;
- overflow-y: auto;
- -webkit-overflow-scrolling: touch;
- h2 {
- margin:15px 0;
- padding:0;
- &:first-of-type {
- margin-top:0;
- }
- > a {
- padding: 10px 0;
- padding-left: 50px;
- padding-right: 20px;
- font-size: 20px;
- }
- }
- a {
- margin: 10px 0;
- padding: 0;
- > h3 {
- clear:both;
- font-size:20px;
- }
- }
- }
-
- .@{ns}-caret {
- color: @gray-dark;
- text-decoration: none;
- // make sure all carats are fixed width
- width: 10px;
- }
-}
-
-
-.@{ns}-accordion-block .@{ns}-accordion-panel-heading {
- padding-left: 26px;
-}
-
-.@{ns}-challenge-title, .@{ns}-accordion-panel-heading {
- background: @body-bg;
- line-height: 26px;
- padding: 2px 12px 2px 10px;
- width:100%;
-}
-
-.@{ns}-challenge-title {
- &::before {
- padding-right: 6px;
- }
- padding-left: 40px;
- a {
- cursor: pointer;
- }
- @media (min-width: 721px) {
- clear: both;
- }
-}
-
-.@{ns}-accordion-panel-heading a {
- cursor: pointer;
- display: block;
-}
-
-.@{ns}-accordion-panel-collapse {
- transition: height 0.001s;
-}
-
-.@{ns}-accordion-panel-title {
- @media (min-width: 721px) {
- clear: both;
- }
-}
-
-.@{ns}-block-time {
- color: #555555;
- @media (min-width: 721px) {
- float: right;
- }
-}
-
-.@{ns}-block-description {
- margin:0;
- margin-top:-10px;
- padding:0 15px 23px 30px;
-}
-
-
-.night {
- .@{ns}-accordion .no-link-underline {
- color: @brand-primary;
- }
- .@{ns}-accordion h2 > a {
- background: #666;
- }
- .@{ns}-accordion a:focus, #noneFound {
- color: #ABABAB;
- }
- .@{ns}-challenge-title, .@{ns}-accordion-panel-heading {
- background: @night-body-bg;
- color: @night-text-color;
- a {
- color: @night-text-color;
- }
- }
- .@{ns}-caret {
- color: @night-text-color;
- }
-}
diff --git a/common/app/Map/ns.json b/common/app/Map/ns.json
deleted file mode 100644
index 6bc66fc09e..0000000000
--- a/common/app/Map/ns.json
+++ /dev/null
@@ -1 +0,0 @@
-"map"
diff --git a/common/app/Map/redux/fetch-map-ui-epic.js b/common/app/Map/redux/fetch-map-ui-epic.js
index 37e7cfa09b..9933f1121e 100644
--- a/common/app/Map/redux/fetch-map-ui-epic.js
+++ b/common/app/Map/redux/fetch-map-ui-epic.js
@@ -3,38 +3,34 @@ import debug from 'debug';
import {
types as appTypes,
- createErrorObservable,
- currentChallengeSelector
+ createErrorObservable
} from '../../redux';
import { types, fetchMapUiComplete } from './';
-import { langSelector } from '../../Router/redux';
import { shapeChallenges } from '../../redux/utils';
const isDev = debug.enabled('fcc:*');
export default function fetchMapUiEpic(
actions,
- { getState },
+ _,
{ services }
) {
- return actions::ofType(
+ return actions.do(console.log)::ofType(
appTypes.appMounted,
types.fetchMapUi.start
)
.flatMapLatest(() => {
- const lang = langSelector(getState());
const options = {
- params: { lang },
service: 'map-ui'
};
return services.readService$(options)
.retry(3)
+ .do(console.info)
.map(({ entities, ...res }) => ({
entities: shapeChallenges(
entities,
isDev
),
- initialNode: currentChallengeSelector(getState()),
...res
}))
.map(fetchMapUiComplete)
diff --git a/common/app/Map/redux/index.js b/common/app/Map/redux/index.js
index f68e67c32e..8fdc57c0a7 100644
--- a/common/app/Map/redux/index.js
+++ b/common/app/Map/redux/index.js
@@ -8,13 +8,14 @@ import { createSelector } from 'reselect';
import { capitalize, noop } from 'lodash';
import * as utils from './utils.js';
-import ns from '../ns.json';
import {
createEventMetaCreator
} from '../../redux';
import fetchMapUiEpic from './fetch-map-ui-epic';
+const ns = 'map';
+
export const epics = [ fetchMapUiEpic ];
export const types = createTypes([
diff --git a/common/app/Nav/LargeNav.jsx b/common/app/Nav/LargeNav.jsx
index d0925bc37d..a1127e3bf4 100644
--- a/common/app/Nav/LargeNav.jsx
+++ b/common/app/Nav/LargeNav.jsx
@@ -2,11 +2,11 @@ import React from 'react';
import Media from 'react-media';
import { Col, Navbar, Row } from 'react-bootstrap';
import FCCSearchBar from 'react-freecodecamp-search';
-import { NavLogo, BinButtons, NavLinks } from './components';
+import { NavLogo, NavLinks } from './components';
import propTypes from './navPropTypes';
-function LargeNav({ clickOnLogo, clickOnMap, shouldShowMapButton, panes }) {
+function LargeNav({ clickOnLogo }) {
return (
-
-
-
+
-
+
diff --git a/common/app/Nav/MediumNav.jsx b/common/app/Nav/MediumNav.jsx
index 559bf623f1..76c84095b5 100644
--- a/common/app/Nav/MediumNav.jsx
+++ b/common/app/Nav/MediumNav.jsx
@@ -2,11 +2,11 @@ import React from 'react';
import Media from 'react-media';
import { Navbar, Row } from 'react-bootstrap';
import FCCSearchBar from 'react-freecodecamp-search';
-import { NavLogo, BinButtons, NavLinks } from './components';
+import { NavLogo, NavLinks } from './components';
import propTypes from './navPropTypes';
-function MediumNav({ clickOnLogo, clickOnMap, shouldShowMapButton, panes }) {
+function MediumNav({ clickOnLogo }) {
return (
matches && typeof window !== 'undefined' && (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
diff --git a/common/app/Nav/Nav.jsx b/common/app/Nav/Nav.jsx
index aaddb015fa..23714fcb0f 100644
--- a/common/app/Nav/Nav.jsx
+++ b/common/app/Nav/Nav.jsx
@@ -1,67 +1,25 @@
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
import { Navbar } from 'react-bootstrap';
import LargeNav from './LargeNav.jsx';
import MediumNav from './MediumNav.jsx';
import SmallNav from './SmallNav.jsx';
import {
- clickOnLogo,
- clickOnMap
+ clickOnLogo
} from './redux';
-import { panesSelector, panesByNameSelector } from '../Panes/redux';
import propTypes from './navPropTypes';
-const mapStateToProps = createSelector(
- panesSelector,
- panesByNameSelector,
- (panes, panesByName) => {
- return {
- panes: panes.map(({ name, type }) => {
- return {
- content: name,
- action: type,
- isHidden: panesByName[name].isHidden
- };
- }, {}),
- shouldShowMapButton: panes.length === 0
- };
- }
-);
+const mapStateToProps = () => ({});
function mapDispatchToProps(dispatch) {
- const dispatchers = bindActionCreators(
+ return bindActionCreators(
{
- clickOnMap: e => {
- e.preventDefault();
- return clickOnMap();
- },
- clickOnLogo: e => {
- e.preventDefault();
- return clickOnLogo();
- }
+ clickOnLogo
},
dispatch
);
- dispatchers.dispatch = dispatch;
- return () => dispatchers;
-}
-
-function mergeProps(stateProps, dispatchProps, ownProps) {
- const panes = stateProps.panes.map(pane => {
- return {
- ...pane,
- actionCreator: () => dispatchProps.dispatch({ type: pane.action })
- };
- });
- return {
- ...ownProps,
- ...stateProps,
- ...dispatchProps,
- panes
- };
}
const allNavs = [
@@ -72,18 +30,12 @@ const allNavs = [
function FCCNav(props) {
const {
- panes,
- clickOnLogo,
- clickOnMap,
- shouldShowMapButton
+ clickOnLogo
} = props;
const withNavProps = Component => (
);
return (
@@ -104,6 +56,5 @@ FCCNav.propTypes = propTypes;
export default connect(
mapStateToProps,
- mapDispatchToProps,
- mergeProps
+ mapDispatchToProps
)(FCCNav);
diff --git a/common/app/Nav/SmallNav.jsx b/common/app/Nav/SmallNav.jsx
index 50b456e1c3..dfa251437e 100644
--- a/common/app/Nav/SmallNav.jsx
+++ b/common/app/Nav/SmallNav.jsx
@@ -2,11 +2,11 @@ import React from 'react';
import Media from 'react-media';
import { Navbar, Row } from 'react-bootstrap';
import FCCSearchBar from 'react-freecodecamp-search';
-import { NavLogo, BinButtons, NavLinks } from './components';
+import { NavLogo, NavLinks } from './components';
import propTypes from './navPropTypes';
-function SmallNav({ clickOnLogo, clickOnMap, shouldShowMapButton, panes }) {
+function SmallNav({ clickOnLogo }) {
return (
matches && typeof window !== 'undefined' && (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
diff --git a/common/app/Nav/components/NavLinks.jsx b/common/app/Nav/components/NavLinks.jsx
index 950d7ed8e8..ac12ba778b 100644
--- a/common/app/Nav/components/NavLinks.jsx
+++ b/common/app/Nav/components/NavLinks.jsx
@@ -8,10 +8,8 @@ import { MenuItem, NavDropdown, NavItem, Nav } from 'react-bootstrap';
import navLinks from '../links.json';
import SignUp from './Sign-Up.jsx';
-import NoPropsPassThrough from '../../utils/No-Props-Passthrough.jsx';
import { Link } from '../../Router';
-import { onRouteCurrentChallenge } from '../../routes/Challenges/redux';
import {
openDropdown,
closeDropdown,
@@ -58,14 +56,12 @@ const navLinkPropType = PropTypes.shape({
const propTypes = {
children: PropTypes.any,
- clickOnMap: PropTypes.func.isRequired,
closeDropdown: PropTypes.func.isRequired,
isDropdownOpen: PropTypes.bool,
isInNav: PropTypes.bool,
isSignedIn: PropTypes.bool,
navLinks: PropTypes.arrayOf(navLinkPropType),
openDropdown: PropTypes.func.isRequired,
- shouldShowMapButton: PropTypes.bool,
showLoading: PropTypes.bool
};
@@ -125,8 +121,6 @@ class NavLinks extends PureComponent {
render() {
const {
- shouldShowMapButton,
- clickOnMap,
showLoading,
isSignedIn,
navLinks,
@@ -136,20 +130,6 @@ class NavLinks extends PureComponent {
return (
);
diff --git a/common/app/routes/Settings/components/Cert-Settings.jsx b/common/app/routes/Settings/components/Cert-Settings.jsx
index c926c4cb51..2e3d1c0c2a 100644
--- a/common/app/routes/Settings/components/Cert-Settings.jsx
+++ b/common/app/routes/Settings/components/Cert-Settings.jsx
@@ -13,7 +13,6 @@ import SectionHeader from './SectionHeader.jsx';
import { projectsSelector } from '../../../entities';
import { claimCert, updateUserBackend } from '../redux';
import {
- fetchChallenges,
userSelector,
hardGoTo,
createErrorObservable
@@ -72,7 +71,6 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators({
claimCert,
createError: createErrorObservable,
- fetchChallenges,
hardGoTo,
updateUserBackend
}, dispatch);
@@ -96,7 +94,6 @@ const propTypes = {
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
claimCert: PropTypes.func.isRequired,
createError: PropTypes.func.isRequired,
- fetchChallenges: PropTypes.func.isRequired,
hardGoTo: PropTypes.func.isRequired,
legacyProjects: projectsTypes,
modernProjects: projectsTypes,
@@ -121,13 +118,6 @@ class CertificationSettings extends PureComponent {
this.handleSubmit = this.handleSubmit.bind(this);
}
- componentDidMount() {
- const { modernProjects } = this.props;
- if (!modernProjects.length) {
- this.props.fetchChallenges();
- }
- }
-
buildProjectForms({
projectBlockName,
challenges,
diff --git a/common/app/routes/Settings/components/Language-Settings.jsx b/common/app/routes/Settings/components/Language-Settings.jsx
deleted file mode 100644
index 16b46be826..0000000000
--- a/common/app/routes/Settings/components/Language-Settings.jsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { createSelector } from 'reselect';
-import { reduxForm } from 'redux-form';
-import {
- FormControl,
- FormGroup,
- ControlLabel,
- Row,
- Col
-} from 'react-bootstrap';
-
-import { updateMyLang } from '../redux';
-import { userSelector } from '../../../redux';
-import langs from '../../../../utils/supported-languages';
-import { FullWidthRow } from '../../../helperComponents';
-import SectionHeader from './SectionHeader.jsx';
-
-const propTypes = {
- fields: PropTypes.object,
- handleSubmit: PropTypes.func.isRequired,
- updateMyLang: PropTypes.func.isRequired
-};
-
-const mapStateToProps = createSelector(
- userSelector,
- ({ languageTag }) => ({
- // send null to prevent redux-form from initialize empty
- initialValues: languageTag ? { lang: languageTag } : null
- })
-);
-const mapDispatchToProps = { updateMyLang };
-const fields = [ 'lang' ];
-const validator = values => {
- if (!langs[values.lang]) {
- return { lang: `${values.lang} is unsupported` };
- }
- return {};
-};
-const options = [(
-
- ),
- ...Object.keys(langs).map(tag => {
- return (
-
- );
- }), (
-
- )
-];
-
-export class LanguageSettings extends React.Component {
- constructor(...props) {
- super(...props);
- this.handleChange = this.handleChange.bind(this);
- }
- componentWillUnmount() {
- // make sure to clear timeout if it exist
- if (this.timeout) {
- clearTimeout(this.timeout);
- }
- }
-
- handleChange(e) {
- e.preventDefault();
- this.props.fields.lang.onChange(e);
- // give redux-form HOC state time to catch up before
- // attempting to submit
- this.timeout = setTimeout(
- () => this.props.handleSubmit(this.props.updateMyLang)(),
- 0
- );
- }
-
- render() {
- const {
- fields: { lang: { name, value } }
- } = this.props;
- return (
-
-
- Language Settings
-
-
-
-
-
-
- Preferred Language for Challenges
-
-
-
-
- { options }
-
-
-
-
-
-
- );
- }
-}
-
-LanguageSettings.displayName = 'LanguageSettings';
-LanguageSettings.propTypes = propTypes;
-
-export default reduxForm(
- {
- form: 'lang',
- fields,
- validator,
- overwriteOnInitialValuesChange: false
- },
- mapStateToProps,
- mapDispatchToProps
-)(LanguageSettings);
diff --git a/common/app/routes/Settings/redux/index.js b/common/app/routes/Settings/redux/index.js
index 5814a6d10e..1128f8d008 100644
--- a/common/app/routes/Settings/redux/index.js
+++ b/common/app/routes/Settings/redux/index.js
@@ -34,7 +34,6 @@ export const types = createTypes([
createAsyncTypes('updateUserBackend'),
createAsyncTypes('deletePortfolio'),
createAsyncTypes('updateMyPortfolio'),
- 'updateMyLang',
'updateNewUsernameValidity',
createAsyncTypes('validateUsername'),
createAsyncTypes('refetchChallengeMap'),
@@ -85,10 +84,6 @@ export const updateMyPortfolioError = createAction(
);
export const deletePortfolio = createAction(types.deletePortfolio.start);
export const deletePortfolioError = createAction(types.deletePortfolio.error);
-export const updateMyLang = createAction(
- types.updateMyLang,
- (values) => values.lang
-);
export const resetProgress = createAction(types.resetProgress.start);
export const resetProgressComplete = createAction(types.resetProgress.complete);
diff --git a/common/app/routes/Settings/redux/update-user-epic.js b/common/app/routes/Settings/redux/update-user-epic.js
index 8604e3d1b4..c999aa96f7 100644
--- a/common/app/routes/Settings/redux/update-user-epic.js
+++ b/common/app/routes/Settings/redux/update-user-epic.js
@@ -3,32 +3,25 @@ import { combineEpics, ofType } from 'redux-epic';
import { pick } from 'lodash';
import {
types,
- onRouteSettings,
refetchChallengeMap,
updateUserBackendComplete,
updateMyPortfolioComplete
} from './';
import { makeToast } from '../../../Toasts/redux';
-import { fetchMapUi } from '../../../Map/redux';
import {
doActionOnError,
usernameSelector,
userSelector,
- createErrorObservable,
- challengeSelector,
- fetchNewBlock
+ createErrorObservable
} from '../../../redux';
import {
updateUserEmail,
- updateUserLang,
updateMultipleUserFlags,
regresPortfolio,
- optoUpdatePortfolio,
- resetFullBlocks
+ optoUpdatePortfolio
} from '../../../entities';
import { postJSON$ } from '../../../../utils/ajax-stream';
-import langs from '../../../../utils/supported-languages';
const endpoints = {
email: '/update-my-email',
@@ -195,52 +188,9 @@ function updateUserEmailEpic(actions, { getState }) {
});
}
-export function updateUserLangEpic(actions, { getState }) {
- const updateLang = actions
- .filter(({ type, payload }) => (
- type === types.updateMyLang && !!langs[payload]
- ))
- .map(({ payload }) => {
- const { languageTag } = userSelector(getState());
- return { lang: payload, oldLang: languageTag };
- });
- const ajaxUpdate = updateLang
- .debounce(250)
- .flatMap(({ lang, oldLang }) => {
- const { app: { user: username, csrfToken: _csrf } } = getState();
- const body = { _csrf, lang };
- return postJSON$('/update-my-lang', body)
- .flatMap(({ message }) => {
- const { block } = challengeSelector(getState());
- return Observable.of(
- // show user that we have updated their lang
- makeToast({ message }),
- // update url to reflect change
- onRouteSettings({ lang }),
- // clear fullBlocks so challenges are fetched in correct language
- resetFullBlocks(),
- // refetch current challenge block updated for new lang
- fetchNewBlock(block),
- // refetch mapUi in new language
- fetchMapUi()
- );
- })
- .catch(doActionOnError(() => {
- return updateUserLang(username, oldLang);
- }));
- });
- const optimistic = updateLang
- .map(({ lang }) => {
- const { app: { user: username } } = getState();
- return updateUserLang(username, lang);
- });
- return Observable.merge(ajaxUpdate, optimistic);
-}
-
export default combineEpics(
backendUserUpdateEpic,
refetchChallengeMapEpic,
updateMyPortfolioEpic,
- updateUserEmailEpic,
- updateUserLangEpic
+ updateUserEmailEpic
);
diff --git a/common/app/routes/index.js b/common/app/routes/index.js
index e007db83d1..36f862f9e9 100644
--- a/common/app/routes/index.js
+++ b/common/app/routes/index.js
@@ -1,35 +1,8 @@
-import { routes as challengesRoutes } from './Challenges';
-import { routes as mapRoutes } from './Map';
import { routes as settingsRoutes } from './Settings';
import { routes as profileRoutes } from './Profile';
-// import { addLang } from '../utils/lang';
-
-export { createPanesMap } from './Challenges';
-
export default {
- ...challengesRoutes,
- ...mapRoutes,
...settingsRoutes,
// ensure profile routes are last else they hijack other routes
...profileRoutes
};
-
-// export default function createChildRoute(deps) {
-// return {
-// path: '/:lang',
-// indexRoute: {
-// onEnter(nextState, replace) {
-// const { lang } = nextState.params;
-// const { pathname } = nextState.location;
-// replace(addLang(pathname, lang));
-// }
-// },
-// childRoutes: [
-// ...challenges(deps),
-// ...map(deps),
-// ...settings(deps),
-// { path: '*', component: NotFound }
-// ]
-// };
-// }
diff --git a/common/app/routes/index.less b/common/app/routes/index.less
index 75b2783b84..d852290ad1 100644
--- a/common/app/routes/index.less
+++ b/common/app/routes/index.less
@@ -1,3 +1,2 @@
-&{ @import "./Challenges/challenges.less"; }
&{ @import "./Profile/profile.less"; }
&{ @import "./Settings/index.less"; }
diff --git a/common/app/routes/redux.js b/common/app/routes/redux.js
index 737987ea24..d25ff41ad8 100644
--- a/common/app/routes/redux.js
+++ b/common/app/routes/redux.js
@@ -1,11 +1,8 @@
import { isLocationAction } from 'redux-first-router';
import { combineReducers } from 'berkeleys-redux-utils';
-
-import challengeReducer from './Challenges/redux';
import profileReducer from './Profile/redux';
import settingsReducer from './Settings/redux';
-import { routes as challengeRoutes } from './Challenges';
import { routes as profileRoutes } from './Profile';
import { routes as settingsRoutes } from './Settings';
@@ -18,9 +15,6 @@ export function mainRouter(state = 'NotFound', action) {
return state;
}
const { type } = action;
- if (challengeRoutes[type]) {
- return 'challenges';
- }
if (settingsRoutes[type]) {
return 'settings';
}
@@ -33,7 +27,6 @@ export function mainRouter(state = 'NotFound', action) {
mainRouter.toString = () => ns;
export default combineReducers(
- challengeReducer,
profileReducer,
settingsReducer,
mainRouter
diff --git a/common/app/utils/lang.js b/common/app/utils/lang.js
deleted file mode 100644
index f727331f66..0000000000
--- a/common/app/utils/lang.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import
- supportedLanguages,
- { langTagRegex }
-from '../../utils/supported-languages';
-
-const toLowerCase = String.prototype.toLowerCase;
-
-export function getLangFromPath(path) {
- const maybeLang = toLowerCase.call(path.split('/')[1]);
- if (supportedLanguages[maybeLang]) {
- return maybeLang;
- }
- return 'en';
-}
-
-export function addLang(path, lang, primaryLang) {
- // if maybeLang is supported continue
- // if maybeLang is unsupported lang, remove and use lang
- // if maybeLang is not lang tag, add lang tag.
- // if both primary and lang are not lang tags default en
- const maybeLang = toLowerCase.call(path.split('/')[1]);
- const restUrl = path.split('/').slice(2).join('/');
-
- if (supportedLanguages[maybeLang]) {
- return path;
- }
-
- if (
- langTagRegex.test(maybeLang) &&
- !supportedLanguages[maybeLang]
- ) {
- return `/${primaryLang || lang }/${restUrl}`;
- }
-
- if (supportedLanguages[primaryLang || lang]) {
- return `/${primaryLang || lang}${path}`;
- }
- return `/en${path}`;
-}
diff --git a/common/app/utils/quotes.js b/common/app/utils/quotes.js
new file mode 100644
index 0000000000..bdc5e35b1f
--- /dev/null
+++ b/common/app/utils/quotes.js
@@ -0,0 +1,143 @@
+const quotes = [
+ {
+ quote: 'Never, never, never, never, never, never give up.',
+ author: 'Winston Churchill'
+ },
+ {
+ quote: 'The pessimist sees difficulty in every opportunity. ' +
+ 'The optimist sees opportunity in every difficulty.',
+ author: 'Winston Churchill'
+ },
+ {
+ quote: 'Twenty years from now you you will be more disappointed by the ' +
+ 'things that you didn\'t do than by the ones you did do. So throw ' +
+ 'off the bowlines. Sail away from the safe harbor. Catch the trade ' +
+ 'winds in your sails.',
+ author: 'Mark Twain'
+ },
+ {
+ quote: 'The secret of getting ahead is getting started.',
+ author: 'Mark Twain'
+ },
+ {
+ quote: 'Change your life today. Don’t gamble on the future, act now, ' +
+ 'without delay.',
+ author: 'Simone de Beauvoir'
+ },
+ {
+ quote: 'A person who never made a mistake never tried anything new.',
+ author: 'Albert Einstein'
+ },
+ {
+ quote: 'Life is like riding a bicycle. To keep your balance, you must ' +
+ 'keep moving.',
+ author: 'Albert Einstein'
+ },
+ {
+ quote: 'Nothing will work unless you do.',
+ author: 'Maya Angelou'
+ },
+ {
+ quote: 'The most difficult thing is the decision to act, the rest is ' +
+ 'merely tenacity.',
+ author: 'Amelia Earhart'
+ },
+ {
+ quote: 'When you reach for the stars, you may not quite get them, but ' +
+ 'you won\'t come up with a handful of mud, either.',
+ author: 'Leo Burnett'
+ },
+ {
+ quote: 'The only person you are destined to become is the person you ' +
+ 'decide to be.',
+ author: 'Ralph Waldo Emerson'
+ },
+ {
+ quote: 'You must do the things you think you cannot do.',
+ author: 'Eleanor Roosevelt'
+ },
+ {
+ quote:
+ 'You are never too old to set another goal, or to dream a new dream.',
+ author: 'C.S. Lewis'
+ },
+ {
+ quote: 'Believe you can and you\'re halfway there.',
+ author: 'Theodore Roosevelt'
+ },
+ {
+ quote:
+ 'I fear not the man who I fear not the man who has practiced 10,000 ' +
+ 'kicks once, but I fear the man who has practiced one kick 10,000 times.',
+ author: 'Bruce Lee'
+ },
+ {
+ quote: 'The way to get started is to quit talking and begin doing.',
+ author: 'Walt Disney'
+ },
+ {
+ quote: 'Don’t let yesterday take up too much of today.',
+ author: 'Will Rodgers'
+ },
+ {
+ quote:
+ 'You learn more from failure than from success. Don’t let it stop you.' +
+ ' Failure builds character.',
+ author: 'Annon'
+ },
+ {
+ quote: 'It’s not whether you get knocked down, it’s whether you get up.',
+ author: 'Vince Lombardi'
+ },
+ {
+ quote:
+ 'If you are working on something that you really care about, you ' +
+ 'don’t have to be pushed. The vision pulls you.',
+ author: 'Steve Jobs'
+ },
+ {
+ quote:
+ 'People who are crazy enough to think they can change the world, are ' +
+ 'the ones who do.',
+ author: 'Rob Siltanen'
+ },
+ {
+ quote:
+ 'Failure will never overtake me if my determination to succeed is ' +
+ 'strong enough.',
+ author: 'Og Mandino'
+ },
+ {
+ quote: 'We may encounter many defeats, but we must not be defeated.',
+ author: 'Maya Angelou'
+ },
+ {
+ quote:
+ 'Knowing is not enough; we must apply. Wishing is not enough; we must ' +
+ 'do.',
+ author: 'Johann Wolfgang von Goethe'
+ },
+ {
+ quote: 'Whether you think you can or think you can’t, you’re right.',
+ author: 'Henry Ford'
+ },
+ {
+ quote:
+ 'The only limit to our realization of tomorrow, will be our doubts of ' +
+ 'today.',
+ author: 'Franklin D. Roosevelt'
+ },
+ {
+ quote:
+ 'You don’t have to be great to start, but you have to start to be great.',
+ author: 'Zig Ziglar'
+ },
+ {
+ quote: 'Today’s accomplishments were yesterday’s impossibilities.',
+ author: 'Robert H. Schuller'
+ }
+];
+
+export function getRandomQuote() {
+ return quotes[Math.floor(Math.random() * quotes.length)];
+}
diff --git a/common/app/utils/redux-first-router.js b/common/app/utils/redux-first-router.js
deleted file mode 100644
index e03cabf4a0..0000000000
--- a/common/app/utils/redux-first-router.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import invariant from 'invariant';
-import { redirect as createRedirect } from 'redux-first-router';
-
-import routesMap from '../routes-map.js';
-import { langSelector } from '../Router/redux';
-
-export function onBeforeChange(dispatch, getState, action) {
- const route = routesMap[action.type];
- const lang = langSelector(getState());
- if (route && route.redirect) {
- invariant(
- typeof route.redirect === 'function',
- `
- route redirect should be a function but got %s
- check the redirect method of route %s
- `,
- route.redirect,
- route
- );
- return dispatch(createRedirect(route.redirect({ lang })));
- }
- return action;
-}
-
-// prevent function from serializing during SSR
-onBeforeChange.toString = () => 'onBeforeChange';
diff --git a/common/models/user.json b/common/models/user.json
index 18949a58ec..bbe76f9d31 100644
--- a/common/models/user.json
+++ b/common/models/user.json
@@ -97,10 +97,6 @@
"type": "string",
"default": ""
},
- "gender": {
- "type": "string",
- "default": ""
- },
"location": {
"type": "string",
"default": ""
@@ -222,6 +218,9 @@
"description": "A map by ID of all the user completed challenges",
"default": {}
},
+ "completedChallengeCount": {
+ "type": "number"
+ },
"completedChallenges": {
"type": [
{
@@ -260,11 +259,6 @@
"type": "string",
"default": "default"
},
- "languageTag": {
- "type": "string",
- "description": "An IETF language tag",
- "default": "en"
- },
"badges": {
"type": {
"coreTeam": {
@@ -369,13 +363,6 @@
"permission": "ALLOW",
"property": "updateTheme"
},
- {
- "accessType": "EXECUTE",
- "principalType": "ROLE",
- "principalId": "$owner",
- "permission": "ALLOW",
- "property": "updateLanguage"
- },
{
"accessType": "EXECUTE",
"principalType": "ROLE",
diff --git a/common/utils/supported-languages.js b/common/utils/supported-languages.js
deleted file mode 100644
index b074d02c43..0000000000
--- a/common/utils/supported-languages.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const langTagRegex = /^[a-z]{2}(?:-[a-zA-Z]{2,3})?$/;
-export default {
- en: 'English',
- es: 'Spanish'
-};
diff --git a/package-lock.json b/package-lock.json
index a7fe759ca3..2e501dffe4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16340,6 +16340,22 @@
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
},
+ "rss-parser": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.1.2.tgz",
+ "integrity": "sha512-mbbLnaxMsFz+2mMpXkOVSecGyCghw+wtBg0IubdVz8N/6jVOpKgImMbWCGQfwH8uRdwyUyeYTN4RpPi6YM6lRQ==",
+ "requires": {
+ "entities": "1.1.1",
+ "xml2js": "0.4.19"
+ },
+ "dependencies": {
+ "entities": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+ "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
+ }
+ }
+ },
"rst-selector-parser": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
diff --git a/package.json b/package.json
index 3f0c743c25..12d8bbf847 100644
--- a/package.json
+++ b/package.json
@@ -139,6 +139,7 @@
"redux-form": "^5.2.3",
"request": "^2.65.0",
"reselect": "^3.0.0",
+ "rss-parser": "^3.1.2",
"rx": "^4.1.0",
"rx-dom": "^7.0.3",
"sanitize-html": "^1.11.1",
diff --git a/server/boot/authentication.js b/server/boot/authentication.js
index 0562d01146..100880e0a4 100644
--- a/server/boot/authentication.js
+++ b/server/boot/authentication.js
@@ -239,6 +239,6 @@ module.exports = function enableAuthentication(app) {
postPasswordlessAuth
);
- app.use('/:lang', router);
+ app.use(router);
app.use(api);
};
diff --git a/server/boot/challenge.js b/server/boot/challenge.js
index 536af52bcf..30a887fe86 100644
--- a/server/boot/challenge.js
+++ b/server/boot/challenge.js
@@ -134,7 +134,7 @@ export default function(app) {
router.get('/map', redirectToLearn);
app.use(api);
- app.use('/:lang', router);
+ app.use(router);
function modernChallengeCompleted(req, res, next) {
const type = accepts(req).type('html', 'json', 'text');
@@ -372,7 +372,7 @@ export default function(app) {
return `${learnURL}/${dasherize(superBlock)}/${block}/${dashedName}`;
})
.subscribe(
- redirect => res._oldRedirect(redirect || learnURL),
+ redirect => res.redirect(redirect || learnURL),
next
);
}
@@ -381,8 +381,8 @@ export default function(app) {
const maybeChallenge = _.last(req.path.split('/'));
if (maybeChallenge in pathMigrations) {
const redirectPath = pathMigrations[maybeChallenge];
- return res.status(302)._oldRedirect(`${learnURL}${redirectPath}`);
+ return res.status(302).redirect(`${learnURL}${redirectPath}`);
}
- return res.status(302)._oldRedirect(learnURL);
+ return res.status(302).redirect(learnURL);
}
}
diff --git a/server/boot/commit.js b/server/boot/commit.js
index 734a5b9019..d5d523217c 100644
--- a/server/boot/commit.js
+++ b/server/boot/commit.js
@@ -82,7 +82,7 @@ export default function commit(app) {
);
app.use(api);
- app.use('/:lang', router);
+ app.use(router);
function commitToNonprofit(req, res, next) {
const { user } = req;
diff --git a/server/boot/home.js b/server/boot/home.js
index 6d2dcc42e7..d32736d63f 100644
--- a/server/boot/home.js
+++ b/server/boot/home.js
@@ -1,44 +1,69 @@
import { defaultProfileImage } from '../../common/utils/constantStrings.json';
-import supportedLanguages from '../../common/utils/supported-languages';
+import { getRandomQuote } from '../../common/app/utils/quotes';
+import { cachedMap } from '../utils/map';
+// import NewsFeed from '../rss';
-const message =
- 'Learn to Code and Help Nonprofits';
+// const news = new NewsFeed();
-module.exports = function(app) {
- var router = app.loopback.Router();
- router.get('/', addDefaultImage, index);
- app.use(
- '/:lang',
- (req, res, next) => {
- // add url language to request for all routers
- req._urlLang = req.params.lang;
- next();
- },
- router
- );
-
- app.use(router);
+module.exports = function(app, done) {
+ const { About } = app.models;
+ const router = app.loopback.Router();
+ let challengeCount = 0;
+ cachedMap(app.models)
+ .do(({ entities: { challenge } }) => {
+ challengeCount = Object.keys(challenge).length;
+ })
+ .subscribe(
+ () => {},
+ err => {throw new Error(err);},
+ () => {
+ router.get('/', addDefaultImage, index);
+ app.use(router);
+ done();
+ }
+ );
function addDefaultImage(req, res, next) {
if (!req.user || req.user.picture) {
return next();
}
return req.user.update$({ picture: defaultProfileImage })
- .subscribe(
- () => next(),
- next
- );
+ .subscribe(
+ () => next(),
+ next
+ );
}
- function index(req, res, next) {
- if (!supportedLanguages[req._urlLang]) {
- return next();
- }
-
- if (req.user) {
- return res.redirect('/challenges/current-challenge');
- }
-
- return res.render('home', { title: message });
- }
+ function index(req, res) {
+ const { user } = req;
+ const homePage = user ? 'userHome' : 'noUserHome';
+ const { quote, author} = getRandomQuote();
+ const title = user ?
+ `Welcome back ${user.name ? user.name : 'Camper'}` :
+ 'Learn to Code and Help Nonprofits';
+ const completedChallengeCount = user && user.completedChallengeCount || 0;
+ const completedProjectCount = user && user.completedProjectCount || 0;
+ const completedCertCount = user && user.completedCertCount || 0;
+ Promise.all([
+ // news.getFeed(),
+ About.getActiveUsersForRendering()
+ ])
+ .then(([
+ // feed,
+ activeUsers
+ ]) => {
+ return res.render(
+ homePage, {
+ activeUsers,
+ author,
+ challengeCount,
+ completedChallengeCount,
+ completedProjectCount,
+ completedCertCount,
+ // feed,
+ quote,
+ title
+ });
+ });
+ }
};
diff --git a/server/boot/randomAPIs.js b/server/boot/randomAPIs.js
index 7419aa6d69..8e79973ccd 100644
--- a/server/boot/randomAPIs.js
+++ b/server/boot/randomAPIs.js
@@ -8,19 +8,18 @@ const githubSecret = process.env.GITHUB_SECRET;
module.exports = function(app) {
const router = app.loopback.Router();
const User = app.models.User;
- const noLangRouter = app.loopback.Router();
- noLangRouter.get('/api/github', githubCalls);
- noLangRouter.get('/chat', chat);
- noLangRouter.get('/twitch', twitch);
- noLangRouter.get('/unsubscribe/:email', unsubscribeAll);
- noLangRouter.get('/unsubscribe-notifications/:email', unsubscribeAll);
- noLangRouter.get('/unsubscribe-quincy/:email', unsubscribeAll);
- noLangRouter.get('/submit-cat-photo', submitCatPhoto);
- noLangRouter.get(
+
+ router.get('/api/github', githubCalls);
+ router.get('/chat', chat);
+ router.get('/twitch', twitch);
+ router.get('/unsubscribe/:email', unsubscribeAll);
+ router.get('/unsubscribe-notifications/:email', unsubscribeAll);
+ router.get('/unsubscribe-quincy/:email', unsubscribeAll);
+ router.get('/submit-cat-photo', submitCatPhoto);
+ router.get(
'/the-fastest-web-page-on-the-internet',
theFastestWebPageOnTheInternet
);
-
router.get('/unsubscribed', unsubscribed);
router.get('/nonprofits', nonprofits);
router.get('/nonprofits-form', nonprofitsForm);
@@ -36,8 +35,7 @@ module.exports = function(app) {
);
router.get('/academic-honesty', academicHonesty);
- app.use(noLangRouter);
- app.use('/:lang', router);
+ app.use(router);
function chat(req, res) {
res.redirect('https://gitter.im/FreeCodeCamp/FreeCodeCamp');
diff --git a/server/boot/react.js b/server/boot/react.js
index c8d6f84cad..e3d5647f43 100644
--- a/server/boot/react.js
+++ b/server/boot/react.js
@@ -48,10 +48,9 @@ export default function reactSubRouter(app) {
});
}
- app.use('/:lang', router);
+ app.use(router);
function serveReactApp(req, res, next) {
- const { lang } = req;
const serviceOptions = { req };
if (req.originalUrl in markupMap) {
log('sending markup from cache');
@@ -68,7 +67,7 @@ export default function reactSubRouter(app) {
devtoolsEnhancer({ name: 'server' })
],
history: createMemoryHistory({ initialEntries: [ req.originalUrl ] }),
- defaultState: { app: { lang } }
+ defaultState: {}
})
.filter(({
location: {
diff --git a/server/boot/settings.js b/server/boot/settings.js
index 0864683219..33e7953094 100644
--- a/server/boot/settings.js
+++ b/server/boot/settings.js
@@ -4,7 +4,6 @@ import {
ifNoUser401,
createValidatorErrorHandler
} from '../utils/middleware';
-import supportedLanguages from '../../common/utils/supported-languages.js';
import { themes } from '../../common/utils/themes.js';
import { alertTypes } from '../../common/utils/flash.js';
@@ -48,29 +47,6 @@ export default function settingsController(app) {
);
}
- function updateMyLang(req, res, next) {
- const { user, body: { lang } = {} } = req;
- const langName = supportedLanguages[lang];
- const update = { languageTag: lang };
- if (!supportedLanguages[lang]) {
- return res.json({
- message: `${lang} is currently unsupported`
- });
- }
- if (user.languageTag === lang) {
- return res.json({
- message: `Your language is already set to ${langName}`
- });
- }
- return user.update$(update)
- .subscribe(
- () => res.json({
- message: `Your language has been updated to '${langName}'`
- }),
- next
- );
- }
-
const updateMyCurrentChallengeValidators = [
check('currentChallengeId')
.isMongoId()
@@ -183,11 +159,6 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger),
updateMyCurrentChallenge
);
- api.post(
- '/update-my-lang',
- ifNoUser401,
- updateMyLang
- );
api.post(
'/update-my-portfolio',
ifNoUser401,
diff --git a/server/boot/user.js b/server/boot/user.js
index 1e901c31a7..50f88bfcd6 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -52,7 +52,7 @@ module.exports = function(app) {
postReportUserProfile
);
- app.use('/:lang', router);
+ app.use(router);
app.use(api);
function getAccount(req, res) {
diff --git a/server/boot/z-lang-redirect.js b/server/boot/z-lang-redirect.js
deleted file mode 100644
index 64be437951..0000000000
--- a/server/boot/z-lang-redirect.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import supportedLanguages from '../../common/utils/supported-languages';
-import passThroughs from '../utils/lang-passthrough-urls';
-import accepts from 'accepts';
-// import debug from 'debug';
-
-// const log = debug('fcc:controller:lang-redirect');
-const toLowerCase = String.prototype.toLowerCase;
-
-export default function redirectLang(app) {
- app.all('*', function(req, res, next) {
- const accept = accepts(req);
- const type = accept.type('html', 'json', 'text');
- const { url, path } = req;
- const langCode = toLowerCase.call(url.split('/')[1]);
-
- if (passThroughs[langCode]) {
- return next();
- }
-
- if (!supportedLanguages[langCode]) {
- // language aware redirect
- return res.redirect(url);
- }
-
- if (type === 'html') {
- req.flash('danger', `We couldn't find path ${ path }`);
- return res.render('404', { title: '404'});
- }
-
- if (type === 'json') {
- return res.status('404').json({ error: 'path not found' });
- }
-
- res.setHeader('Content-Type', 'text/plain');
- return res.send('404 path not found');
- });
-}
diff --git a/server/boot/z-not-found.js b/server/boot/z-not-found.js
new file mode 100644
index 0000000000..0f517f6e67
--- /dev/null
+++ b/server/boot/z-not-found.js
@@ -0,0 +1,22 @@
+import accepts from 'accepts';
+
+export default function fourOhFour(app) {
+ app.all('*', function(req, res) {
+ const accept = accepts(req);
+ const type = accept.type('html', 'json', 'text');
+ const { path } = req;
+
+
+ if (type === 'html') {
+ req.flash('danger', `We couldn't find path ${ path }`);
+ return res.render('404', { title: '404'});
+ }
+
+ if (type === 'json') {
+ return res.status('404').json({ error: 'path not found' });
+ }
+
+ res.setHeader('Content-Type', 'text/plain');
+ return res.send('404 path not found');
+ });
+}
diff --git a/server/component-passport.js b/server/component-passport.js
index f5c925f344..d28e9bee5f 100644
--- a/server/component-passport.js
+++ b/server/component-passport.js
@@ -9,10 +9,21 @@ const passportOptions = {
const fields = {
progressTimestamps: false,
- completedChallenges: false,
- challengeMap: false
+ completedChallenges: false
};
+function getCompletedCertCount(user) {
+ return [
+ 'isApisMicroservicesCert',
+ 'is2018DataVisCert',
+ 'isFrontEndLibsCert',
+ 'isInfosecQaCert',
+ 'isJsAlgoDataStructCert',
+ 'isRespWebDesignCert'
+ ].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
+
+}
+
PassportConfigurator.prototype.init = function passportInit(noSession) {
this.app.middleware('session:after', passport.initialize());
@@ -33,8 +44,10 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
this.userModel.findById(id, { fields }, (err, user) => {
if (err || !user) {
+ user.challengeMap = {};
return done(err, user);
}
+
return this.app.dataSources.db.connector
.collection('user')
.aggregate([
@@ -43,6 +56,25 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
], function(err, [{ points = 1 } = {}]) {
if (err) { return done(err); }
user.points = points;
+ let completedChallengeCount = 0;
+ let completedProjectCount = 0;
+ if ('challengeMap' in user) {
+ completedChallengeCount = Object.keys(user.challengeMap).length;
+ Object.keys(user.challengeMap)
+ .map(key => user.challengeMap[key])
+ .forEach(item => {
+ if (
+ 'challengeType' in item &&
+ (item.challengeType === 3 || item.challengeType === 4)
+ ) {
+ completedProjectCount++;
+ }
+ });
+ }
+ user.completedChallengeCount = completedChallengeCount;
+ user.completedProjectCount = completedProjectCount;
+ user.completedCertCount = getCompletedCertCount(user);
+ user.challengeMap = {};
return done(null, user);
});
});
diff --git a/server/middleware.json b/server/middleware.json
index a9d377440d..6afd88a9c4 100644
--- a/server/middleware.json
+++ b/server/middleware.json
@@ -55,7 +55,6 @@
"./middlewares/csp": {},
"./middlewares/jade-helpers": {},
"./middlewares/migrate-completed-challenges": {},
- "./middlewares/add-lang": {},
"./middlewares/flash-cheaters": {},
"./middlewares/passport-login": {}
},
diff --git a/server/middlewares/add-lang.js b/server/middlewares/add-lang.js
deleted file mode 100644
index eb159664aa..0000000000
--- a/server/middlewares/add-lang.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import
- supportedLanguages,
- { langTagRegex }
-from '../../common/utils/supported-languages';
-import passthroughs from '../utils/lang-passthrough-urls';
-import debug from 'debug';
-
-const log = debug('fcc:middlewares:lang');
-const toLowerCase = String.prototype.toLowerCase;
-
-// redirect(statusOrUrl: String|Number, url?: String) => Void
-function langRedirect(...args) {
- const url = args.length === 2 ? args[1] : args[0];
- const { lang } = this.req;
- const maybeLang = toLowerCase.call((url.split('/')[1] || ''));
-
- if (
- passthroughs[maybeLang] ||
- supportedLanguages[maybeLang]
- ) {
- return this._oldRedirect(...arguments);
- }
-
- // if language present add to url
- if (lang) {
- return this._oldRedirect(`/${lang}${url}`);
- }
-
- // default to english
- return this._oldRedirect(`/en${url}`);
-}
-
-// prefer url lang over user lang
-// if url lang is not supported move to user lang
-// if user lang is not supported default to english
-export default function addLang() {
- return function(req, res, next) {
- const { url, user = {} } = req;
- const maybeLang = url.split('/')[1];
- const restUrl = url.split('/').slice(2).join('/');
- const userLang = user.languageTag;
- let finalLang;
- if (supportedLanguages[maybeLang]) {
- finalLang = maybeLang;
- } else if (supportedLanguages[userLang]) {
- finalLang = userLang;
- } else {
- finalLang = 'en';
- }
- // found url lang tag that is not yet supported
- // redirect to fix url with supported lang tag
- if (langTagRegex.test(maybeLang) && !supportedLanguages[maybeLang]) {
- log(`unsupported lang tag ${maybeLang}`);
- return res.redirect(`/${finalLang}/${restUrl}`);
- }
- res.locals.supportedLanguages = supportedLanguages;
-
- if (supportedLanguages[finalLang]) {
- req.lang = finalLang;
- res.locals.lang = finalLang;
- }
-
- res._oldRedirect = res.redirect;
- res.redirect = langRedirect;
-
- return next();
- };
-}
diff --git a/server/middlewares/add-return-to.js b/server/middlewares/add-return-to.js
index 1a74d03d59..e117f9d877 100644
--- a/server/middlewares/add-return-to.js
+++ b/server/middlewares/add-return-to.js
@@ -35,8 +35,8 @@ export default function addReturnToUrl() {
) {
return next();
}
- req.session.returnTo = req.originalUrl === '/map-aside' ?
- '/map' :
+ req.session.returnTo = req.originalUrl.includes('/map') ?
+ '/' :
req.originalUrl;
return next();
};
diff --git a/server/models/about.js b/server/models/about.js
index 58be09b0e4..1ea5037709 100644
--- a/server/models/about.js
+++ b/server/models/about.js
@@ -3,12 +3,17 @@ import { createActiveUsers } from '../utils/about.js';
module.exports = function(About) {
const activeUsers = createActiveUsers();
- About.getActiveUsers = function getActiveUsers() {
+ let activeUsersForRendering = 0;
+ About.getActiveUsers = async function getActiveUsers() {
// converting to promise automatically will subscribe to Observable
// initiating the sequence above
- return activeUsers.toPromise();
+ const usersActive = await activeUsers.toPromise();
+ activeUsersForRendering = usersActive;
+ return usersActive;
};
+ About.getActiveUsersForRendering = () => activeUsersForRendering;
+
About.remoteMethod(
'getActiveUsers',
{
diff --git a/server/rss/index.js b/server/rss/index.js
new file mode 100644
index 0000000000..5fd8f2556c
--- /dev/null
+++ b/server/rss/index.js
@@ -0,0 +1,86 @@
+import _ from 'lodash';
+import compareDesc from 'date-fns/compare_desc';
+import debug from 'debug';
+
+import { getMediumFeed } from './medium';
+import { getLybsynFeed } from './lybsyn';
+
+const log = debug('fcc:rss:news-feed');
+
+const fiveMinutes = 1000 * 60 * 5;
+
+class NewsFeed {
+ constructor() {
+
+ this.state = {
+ readyState: false,
+ mediumFeed: [],
+ lybsynFeed: [],
+ combinedFeed: []
+ };
+ this.refreshFeeds();
+
+ setInterval(this.refreshFeeds, fiveMinutes);
+ }
+
+ setState = stateUpdater => {
+ const newState = stateUpdater(this.state);
+ this.state = _.merge({}, this.state, newState);
+ return;
+ }
+
+ refreshFeeds = () => {
+ const currentFeed = this.state.combinedFeed.slice(0);
+ log('grabbing feeds');
+ return Promise.all([
+ getMediumFeed(),
+ getLybsynFeed()
+ ]).then(
+ ([mediumFeed, lybsynFeed]) => this.setState(
+ state => ({
+ ...state,
+ mediumFeed,
+ lybsynFeed
+ })
+ ))
+ .then(() => {
+ log('crossing the streams');
+ const { mediumFeed, lybsynFeed} = this.state;
+ const combinedFeed = [ ...mediumFeed, ...lybsynFeed ].sort((a, b) => {
+ return compareDesc(a.isoDate, b.isoDate);
+ });
+ this.setState(state => ({
+ ...state,
+ combinedFeed,
+ readyState: true
+ }));
+ })
+ .catch(err => {
+ console.log(err);
+ this.setState(state => ({
+ ...state,
+ combinedFeed: currentFeed
+ }));
+ });
+ }
+
+
+ getFeed = () => new Promise((resolve) => {
+ let notReadyCount = 0;
+
+ function waitForReady() {
+ log('notReadyCount', notReadyCount);
+ notReadyCount++;
+ return this.state.readyState || notReadyCount === 5 ?
+ resolve(this.state.combinedFeed) :
+ setTimeout(waitForReady, 100);
+ }
+ log('are we ready?', this.state.readyState);
+ return this.state.readyState ?
+ resolve(this.state.combinedFeed) :
+ setTimeout(waitForReady, 100);
+ })
+
+}
+
+export default NewsFeed;
diff --git a/server/rss/lybsyn.js b/server/rss/lybsyn.js
new file mode 100644
index 0000000000..f4797b74f8
--- /dev/null
+++ b/server/rss/lybsyn.js
@@ -0,0 +1,47 @@
+import http from 'http';
+import _ from 'lodash';
+
+const lybsynFeed = 'http://freecodecamp.libsyn.com/render-type/json';
+
+export function getLybsynFeed() {
+ return new Promise((resolve, reject) => {
+ http.get(lybsynFeed, res => {
+ let raw = '';
+
+ res.on('data', chunk => {
+ raw += chunk;
+ });
+
+ res.on('error', err => reject(err));
+
+ res.on('end', () => {
+ let feed = [];
+
+ try {
+ feed = JSON.parse(raw);
+ } catch (err) {
+ return reject(err);
+ }
+ const items = feed.map(
+ item => _.pick(item, [
+ 'full_item_url',
+ 'item_title',
+ 'release_date',
+ 'item_body_short'
+ ])
+ )
+ /* eslint-disable camelcase */
+ .map(({ full_item_url, item_title, release_date, item_body_short}) => ({
+ title: item_title,
+ extract: item_body_short,
+ isoDate: new Date(release_date).toISOString(),
+ link: full_item_url
+ }));
+ /* eslint-enable camelcase */
+ return resolve(items);
+ });
+
+ });
+
+ });
+}
diff --git a/server/rss/medium.js b/server/rss/medium.js
new file mode 100644
index 0000000000..979b4c9fba
--- /dev/null
+++ b/server/rss/medium.js
@@ -0,0 +1,39 @@
+import Parser from 'rss-parser';
+import _ from 'lodash';
+
+const parser = new Parser();
+
+const mediumFeed = 'https://medium.freecodecamp.org/feed';
+
+function getExtract(str) {
+ return str.slice(0, str.indexOf('') + 4);
+}
+
+
+function addResponsiveClass(str) {
+ return str.replace(/\
{
+ parser.parseURL(mediumFeed, (err, feed) => {
+ if (err) {
+ reject(err);
+ }
+
+ const items = feed.items
+ .map(
+ item => _.pick(item, ['title', 'link', 'isoDate', 'content:encoded'])
+ )
+ .map(
+ (item) => ({
+ ...item,
+ extract: getExtract(item['content:encoded'])
+ })
+ )
+ .map(item => _.omit(item, ['content:encoded']))
+ .map(item => ({ ...item, extract: addResponsiveClass(item.extract)}));
+ resolve(items);
+ });
+ });
+}
diff --git a/server/services/challenge.js b/server/services/challenge.js
index 53b92fbdb2..6804ae153e 100644
--- a/server/services/challenge.js
+++ b/server/services/challenge.js
@@ -2,7 +2,7 @@ import debug from 'debug';
import { pickBy } from 'lodash';
import { Observable } from 'rx';
-import { cachedMap, getMapForLang, getChallenge } from '../utils/map';
+import { cachedMap, getChallenge } from '../utils/map';
import { shapeChallenges } from '../../common/app/redux/utils';
const log = debug('fcc:services:challenge');
@@ -15,11 +15,11 @@ export default function getChallengesForBlock(app) {
read: function readChallengesForBlock(
req,
resource,
- { dashedName, blockName, lang = 'en' } = {},
+ { dashedName, blockName} = {},
config,
cb
) {
- const getChallengeBlock$ = challengeMap.map(getMapForLang(lang))
+ const getChallengeBlock$ = challengeMap
.flatMap(({
result: { superBlocks },
entities: {
@@ -46,7 +46,7 @@ export default function getChallengesForBlock(app) {
});
return Observable.if(
() => !!dashedName,
- getChallenge(dashedName, blockName, challengeMap, lang),
+ getChallenge(dashedName, blockName, challengeMap, 'en'),
getChallengeBlock$
)
.subscribe(
diff --git a/server/services/mapUi.js b/server/services/mapUi.js
index adb313b006..e3a309cc7e 100644
--- a/server/services/mapUi.js
+++ b/server/services/mapUi.js
@@ -1,23 +1,17 @@
import debug from 'debug';
import { Observable } from 'rx';
-import { cachedMap, getMapForLang } from '../utils/map';
+import { cachedMap } from '../utils/map';
const log = debug('fcc:services:mapUi');
export default function mapUiService(app) {
- const supportedLangMap = {};
const challengeMap = cachedMap(app.models);
return {
name: 'map-ui',
- read: function readMapUi(req, resource, { lang = 'en' } = {}, config, cb) {
- log(`generating mapUi for ${lang}`);
- if (lang in supportedLangMap) {
- log(`using cache for ${lang} map`);
- return cb(null, supportedLangMap[lang]);
- }
- return challengeMap.map(getMapForLang(lang))
+ read: function readMapUi(req, resource, _, config, cb) {
+ return challengeMap
.flatMap(({
result: { superBlocks },
entities: {
@@ -82,7 +76,6 @@ export default function mapUiService(app) {
challenge: challengeMap
}
};
- supportedLangMap[lang] = mapUi;
return Observable.of(mapUi);
}).subscribe(
mapUi => cb(null, mapUi ),
diff --git a/server/utils/map.js b/server/utils/map.js
index 99b67d159a..7693d719a8 100644
--- a/server/utils/map.js
+++ b/server/utils/map.js
@@ -2,7 +2,6 @@ import _ from 'lodash';
import { Observable } from 'rx';
import { unDasherize, nameify } from '../utils';
-import supportedLanguages from '../../common/utils/supported-languages';
import {
addNameIdMap as _addNameIdToMap,
checkMapData,
@@ -118,42 +117,6 @@ export function _cachedMap({ Block, Challenge }) {
export const cachedMap = _.once(_cachedMap);
-export function mapChallengeToLang(
- { translations = {}, ...challenge },
- lang
-) {
- if (!supportedLanguages[lang]) {
- lang = 'en';
- }
- const translation = translations[lang] || {};
- const isTranslated = Object.keys(translation).length > 0;
- if (lang !== 'en') {
- challenge = {
- ...challenge,
- ...translation,
- isTranslated
- };
- }
- return {
- ...challenge,
- isTranslated
- };
-}
-
-export function getMapForLang(lang) {
- return ({ entities: { challenge: challengeMap, ...entities }, result }) => {
- entities.challenge = Object.keys(challengeMap)
- .reduce((translatedChallengeMap, key) => {
- translatedChallengeMap[key] = mapChallengeToLang(
- challengeMap[key],
- lang
- );
- return translatedChallengeMap;
- }, {});
- return { result, entities };
- };
-}
-
// type ObjectId: String;
// getChallengeById(
// map: Observable[map],
@@ -208,9 +171,7 @@ function loadComingSoonOrBetaChallenge({
export function getChallenge(
challengeDashedName,
blockDashedName,
- map,
- lang
-) {
+ map) {
return map
.flatMap(({ entities, result: { superBlocks } }) => {
const superBlock = entities.superBlock;
@@ -233,7 +194,7 @@ export function getChallenge(
entities: {
superBlock,
challenge: {
- [challenge.dashedName]: mapChallengeToLang(challenge, lang)
+ [challenge.dashedName]: challenge
}
},
result: {
diff --git a/server/utils/publicUserProps.js b/server/utils/publicUserProps.js
index 36a153234d..5016d960a0 100644
--- a/server/utils/publicUserProps.js
+++ b/server/utils/publicUserProps.js
@@ -43,7 +43,6 @@ export const userPropsForSession = [
'currentChallengeId',
'email',
'id',
- 'languageTag',
'sendQuincyEmail',
'theme'
];
diff --git a/server/views/homePartials/activeUsersScript.jade b/server/views/homePartials/activeUsersScript.jade
new file mode 100644
index 0000000000..9e0d7fbc2f
--- /dev/null
+++ b/server/views/homePartials/activeUsersScript.jade
@@ -0,0 +1,14 @@
+script.
+ $(document).ready(function getActiveUsers() {
+ setInterval(function getActiveUsersInterval() {
+ $.ajax({
+ type : 'GET',
+ url : '/api/about/get-active-users',
+ })
+ .done(function({ activeUsers }) {
+ document.getElementById('active-user-count').innerHTML = `
+ ${activeUsers} people are using freeCodeCamp right now
+ `;
+ })
+ }, 2000)
+ })
\ No newline at end of file
diff --git a/server/views/homePartials/activeUsersView.jade b/server/views/homePartials/activeUsersView.jade
new file mode 100644
index 0000000000..66c4282470
--- /dev/null
+++ b/server/views/homePartials/activeUsersView.jade
@@ -0,0 +1,4 @@
+#active-user-count
+ h3
+ span.green-text=activeUsers
+ | people are using freeCodeCamp right now
\ No newline at end of file
diff --git a/server/views/homePartials/newsFeed.jade b/server/views/homePartials/newsFeed.jade
new file mode 100644
index 0000000000..d0ad2461a0
--- /dev/null
+++ b/server/views/homePartials/newsFeed.jade
@@ -0,0 +1,18 @@
+.row
+ .col-xs-12.col-sm-8.col-sm-offset-2
+ h2 News Feed
+ ul.list-group
+ each item in feed
+ li.list-group-item
+ a(href=item.link)
+ h3=item.title
+ div !{item.extract}
+ .spacer
+.row
+ .col-xs-12.col-sm-8.col-sm-offset-2
+ .more-button-container
+ div
+ button.btn.btn-primary.btn-lg(href='https://medium.freecodecamp.org') More Articles
+ div
+ button.btn.btn-primary.btn-lg(href='https://freecodecamp.libsyn.com/') More Podcasts
+
diff --git a/server/views/homePartials/quote.jade b/server/views/homePartials/quote.jade
new file mode 100644
index 0000000000..0a435f4434
--- /dev/null
+++ b/server/views/homePartials/quote.jade
@@ -0,0 +1,6 @@
+.row
+ .col-xs-12.col-sm-8.col-sm-offset-2
+ blockquote.blockquote
+ span=quote
+ footer
+ cite.blockquote-footer=author
\ No newline at end of file
diff --git a/server/views/homePartials/scripts.jade b/server/views/homePartials/scripts.jade
new file mode 100644
index 0000000000..34f9ce1b4b
--- /dev/null
+++ b/server/views/homePartials/scripts.jade
@@ -0,0 +1,3 @@
+script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js')
+script(src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js')
+include activeUsersScript
\ No newline at end of file
diff --git a/server/views/homePartials/stats.jade b/server/views/homePartials/stats.jade
new file mode 100644
index 0000000000..dc913c03ef
--- /dev/null
+++ b/server/views/homePartials/stats.jade
@@ -0,0 +1,26 @@
+.row
+ .col-xs-12.col-sm-8.col-sm-offset-2
+ p.stats
+ | You have completed
+ strong
+ span.green-text=completedChallengeCount
+ | of
+ strong
+ span.green-text=challengeCount
+ | lessons.
+ p.stats
+ | You have built
+ strong
+ span.green-text=completedProjectCount
+ | out of
+ strong
+ span.green-text 30
+ | projects
+ p.stats
+ | You have earned
+ strong
+ span.green-text=completedCertCount
+ | out of
+ strong
+ span.green-text 6
+ | certifications
\ No newline at end of file
diff --git a/server/views/home.jade b/server/views/noUserHome.jade
similarity index 95%
rename from server/views/home.jade
rename to server/views/noUserHome.jade
index 0fd39620ca..db3db52b16 100644
--- a/server/views/home.jade
+++ b/server/views/noUserHome.jade
@@ -4,6 +4,7 @@ block content
.row
h1.landing-heading Learn to code for free.
.big-break
+ include homePartials/activeUsersView
.big-break
.big-break
.col-xs-12.col-sm-12.col-md-4
@@ -82,3 +83,11 @@ block content
.col-xs-12.col-sm-8.col-sm-offset-2
a.btn.btn-cta.signup-btn.btn-block(href="/signin") Start coding (it's free)
.big-break
+ .big-break
+ include homePartials/quote
+ .big-break
+ hr
+ .big-break
+ .big-break
+ .big-break
+ include homePartials/scripts
\ No newline at end of file
diff --git a/server/views/partials/navbar.jade b/server/views/partials/navbar.jade
index 6b2bf735ae..a866c4ee57 100644
--- a/server/views/partials/navbar.jade
+++ b/server/views/partials/navbar.jade
@@ -4,32 +4,17 @@ nav.navbar.navbar-default.navbar-static-top.nav-height
button.hamburger.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse')
.col-xs-12
span.hamburger-text Menu
- a.navbar-brand(href='/challenges/current-challenge')
+ a.navbar-brand(href='/')
img.img-responsive.nav-logo(src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt='learn to code javascript at freeCodeCamp logo')
.collapse.navbar-collapse
ul.nav.navbar-nav.navbar-right.hamburger-dropdown
li
- a(href='/map') Map
- li.dropdown
- a.dropdown-toggle(data-toggle='dropdown' role='button' href='#' aria-haspopup='true') Community
- ul.dropdown-menu
- li
- a(href='https://gitter.im/freecodecamp/home' target='_blank' rel='noopener') Chat
- li
- a(href='https://forum.freecodecamp.org', target='_blank' rel='noopener') Forum
- li
- a(href='https://medium.freecodecamp.org', target='_blank' rel='noopener') Medium
- li
- a(href='https://youtube.com/freecodecamp', target='_blank' rel='noopener') YouTube
- li
- a(href='https://forum.freecodecamp.org/t/free-code-camp-city-based-local-groups/19574', target='_blank' rel='noopener') In your city
- li
- a(href='/about') About
+ a(href='https://learn.freecodecamp.org' target='_blank' rel='noopener') Learn
li
- a(href='https://www.freecodecamp.org/donate', target='_blank' rel='noopener') Donate
+ a(href='https://forum.freecodecamp.org', target='_blank' rel='noopener') Forum
if !user
li
a(href='/signin') Start Coding
else
li
- a(href='/settings') My Profile
+ a(href='/settings') Settings
diff --git a/server/views/userHome.jade b/server/views/userHome.jade
new file mode 100644
index 0000000000..b1528ff831
--- /dev/null
+++ b/server/views/userHome.jade
@@ -0,0 +1,22 @@
+extends layout
+block content
+ .text-center
+ .row
+ h1.landing-heading=title
+ .big-break
+ include homePartials/activeUsersView
+ .big-break
+ .big-break
+ include homePartials/quote
+ .big-break
+ .big-break
+ include homePartials/stats
+ .big-break
+ .big-break
+ .row
+ .col-xs-12.col-sm-8.col-sm-offset-2
+ a.btn.btn-cta.btn-block.btn-lg.btn-primary(href="/challenges/current-challenge") Go to my next lesson
+ .big-break
+ .big-break
+ .big-break
+ include homePartials/scripts
\ No newline at end of file