diff --git a/client/index.js b/client/index.js index 96e6e9ca92..296870365f 100644 --- a/client/index.js +++ b/client/index.js @@ -45,7 +45,8 @@ const sagaOptions = { isDev, window, document: window.document, - location: window.location + location: window.location, + history: window.history }; diff --git a/client/sagas/hard-go-to-saga.js b/client/sagas/hard-go-to-saga.js index d02cd69d6a..46bfd15289 100644 --- a/client/sagas/hard-go-to-saga.js +++ b/client/sagas/hard-go-to-saga.js @@ -1,16 +1,10 @@ import { hardGoTo } from '../../common/app/redux/types'; -export default function hardGoToSaga(action$, getState, { location }) { +export default function hardGoToSaga(action$, getState, { history }) { return action$ .filter(({ type }) => type === hardGoTo) - .map(({ payload = '/map' }) => { - if (!location.pathname) { - return { - type: 'app.error', - error: new Error('no location object found') - }; - } - location.pathname = payload; + .map(({ payload = '/settings' }) => { + history.push(history.state, null, payload); return null; }); } diff --git a/client/sagas/index.js b/client/sagas/index.js index 54cd65a21b..d9fa142999 100644 --- a/client/sagas/index.js +++ b/client/sagas/index.js @@ -6,6 +6,7 @@ import executeChallengeSaga from './execute-challenge-saga'; import frameSaga from './frame-saga'; import codeStorageSaga from './code-storage-saga'; import gitterSaga from './gitter-saga'; +import mouseTrapSaga from './mouse-trap-saga'; export default [ errSaga, @@ -15,5 +16,6 @@ export default [ executeChallengeSaga, frameSaga, codeStorageSaga, - gitterSaga + gitterSaga, + mouseTrapSaga ]; diff --git a/client/sagas/mouse-trap-saga.js b/client/sagas/mouse-trap-saga.js new file mode 100644 index 0000000000..a9b3146ce7 --- /dev/null +++ b/client/sagas/mouse-trap-saga.js @@ -0,0 +1,41 @@ +import { Observable } from 'rx'; +import MouseTrap from 'mousetrap'; +import { push } from 'react-router-redux'; +import { + toggleNightMode, + toggleMapDrawer, + toggleMainChat, + hardGoTo +} from '../../common/app/redux/actions'; + +function bindKey$(key, actionCreator) { + return Observable.fromEventPattern( + h => MouseTrap.bind(key, h), + h => MouseTrap.unbind(key, h) + ) + .map(actionCreator); +} + +const softRedirects = { + 'g n n': '/challenges/next-challenge', + 'g n a': '/about', + 'g n m': '/map', + 'g n w': '/wiki', + 'g n s': '/shop', + 'g n o': '/settings' +}; + +export default function mouseTrapSaga(actions$) { + const traps$ = [ + ...Object.keys(softRedirects) + .map(key => bindKey$(key, () => push(softRedirects[key]))), + bindKey$( + 'g n r', + () => hardGoTo('https://github.com/freecodecamp/freecodecamp') + ), + bindKey$('g m', toggleMapDrawer), + bindKey$('g t n', toggleNightMode), + bindKey$('g c', toggleMainChat) + ]; + return Observable.merge(traps$).takeUntil(actions$.last()); +} diff --git a/common/app/components/NotFound/index.jsx b/common/app/components/NotFound/index.jsx index 0032277c7e..7b3e67de43 100644 --- a/common/app/components/NotFound/index.jsx +++ b/common/app/components/NotFound/index.jsx @@ -1,26 +1,22 @@ import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { hardGoTo } from '../../redux/actions'; -const win = typeof window !== 'undefined' ? window : {}; - -function goToServer(path) { - win.location = '/' + path; -} - -export default class extends React.Component { +export class NotFound extends React.Component { static displayName = 'NotFound'; static propTypes = { - params: PropTypes.object + location: PropTypes.object, + hardGoTo: PropTypes.func }; componentWillMount() { - goToServer(this.props.params.splat); - } - - componentDidMount() { + this.props.hardGoTo(this.props.location.pathname); } render() { return ; } } + +export default connect(null, { hardGoTo })(NotFound); diff --git a/common/app/redux/actions.js b/common/app/redux/actions.js index 2b6f45f3b0..281c937080 100644 --- a/common/app/redux/actions.js +++ b/common/app/redux/actions.js @@ -56,3 +56,4 @@ export const toggleWikiDrawer = createAction(types.toggleWikiDrawer); // chat export const toggleMainChat = createAction(types.toggleMainChat); +export const toggleNightMode = createAction(types.toggleNightMode); diff --git a/common/app/redux/types.js b/common/app/redux/types.js index d9914ce825..8338e9ff69 100644 --- a/common/app/redux/types.js +++ b/common/app/redux/types.js @@ -9,6 +9,7 @@ export default createTypes([ 'makeToast', 'updatePoints', 'handleError', + 'toggleNightMode', // used to hit the server 'hardGoTo', 'delayedRedirect', diff --git a/common/app/routes/index.js b/common/app/routes/index.js index 67e6e2bac0..0d2d3928b1 100644 --- a/common/app/routes/index.js +++ b/common/app/routes/index.js @@ -1,8 +1,16 @@ import { modernChallenges, map, challenges } from './challenges'; import NotFound from '../components/NotFound/index.jsx'; +import { addLang } from '../utils/add-lang'; export default { path: '/:lang', + indexRoute: { + onEnter(nextState, replace) { + const { lang } = nextState.params; + const { pathname } = nextState.location; + replace(addLang(pathname, lang)); + } + }, childRoutes: [ challenges, modernChallenges, diff --git a/common/app/utils/Language-Link.jsx b/common/app/utils/Language-Link.jsx index 0d6cf0d445..8caf5f2610 100644 --- a/common/app/utils/Language-Link.jsx +++ b/common/app/utils/Language-Link.jsx @@ -1,19 +1,7 @@ import React, { PropTypes } from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router'; -import supportedLanguages from '../../utils/supported-languages'; - -const toLowerCase = String.prototype.toLowerCase; -function addLang(url, lang) { - const maybeLang = toLowerCase.call(url.split('/')[1]); - if (supportedLanguages[maybeLang]) { - return url; - } - if (supportedLanguages[lang]) { - return `/${lang}${url}`; - } - return `/en${url}`; -} +import { addLang } from './add-lang'; const mapStateToProps = state => ({ lang: state.app.lang }); diff --git a/common/app/utils/add-lang.js b/common/app/utils/add-lang.js new file mode 100644 index 0000000000..ef66a4a2b5 --- /dev/null +++ b/common/app/utils/add-lang.js @@ -0,0 +1,13 @@ +import supportedLanguages from '../../utils/supported-languages'; + +const toLowerCase = String.prototype.toLowerCase; +export function addLang(url, lang) { + const maybeLang = toLowerCase.call(url.split('/')[1]); + if (supportedLanguages[maybeLang]) { + return url; + } + if (supportedLanguages[lang]) { + return `/${lang}${url}`; + } + return `/en${url}`; +} diff --git a/package.json b/package.json index dff29f1bb5..3b44b07a15 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "moment-timezone": "^0.5.0", "mongodb": "^2.0.33", "morgan": "^1.6.1", + "mousetrap": "^1.6.0", "node-uuid": "^1.4.3", "nodemailer": "^2.1.0", "nodemailer-ses-transport": "^1.3.0",