Merge pull request #14708 from freeCodeCamp/revert-13592-feature/youtubeInMap
Revert "Add Youtube videos to the map"
This commit is contained in:
@ -6,7 +6,6 @@ import { createSelector } from 'reselect';
|
|||||||
import ns from './ns.json';
|
import ns from './ns.json';
|
||||||
import {
|
import {
|
||||||
fetchUser,
|
fetchUser,
|
||||||
fetchYoutube,
|
|
||||||
updateAppLang,
|
updateAppLang,
|
||||||
trackEvent,
|
trackEvent,
|
||||||
loadCurrentChallenge,
|
loadCurrentChallenge,
|
||||||
@ -23,7 +22,6 @@ import { userSelector } from './redux/selectors';
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
closeDropdown,
|
closeDropdown,
|
||||||
fetchUser,
|
fetchUser,
|
||||||
fetchYoutube,
|
|
||||||
loadCurrentChallenge,
|
loadCurrentChallenge,
|
||||||
openDropdown,
|
openDropdown,
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
@ -37,13 +35,11 @@ const mapStateToProps = createSelector(
|
|||||||
state => state.app.isSignInAttempted,
|
state => state.app.isSignInAttempted,
|
||||||
state => state.app.toast,
|
state => state.app.toast,
|
||||||
state => state.challengesApp.toast,
|
state => state.challengesApp.toast,
|
||||||
state => state.entities.youtube,
|
|
||||||
(
|
(
|
||||||
{ user: { username, points, picture } },
|
{ user: { username, points, picture } },
|
||||||
isNavDropdownOpen,
|
isNavDropdownOpen,
|
||||||
isSignInAttempted,
|
isSignInAttempted,
|
||||||
toast,
|
toast,
|
||||||
youtube
|
|
||||||
) => ({
|
) => ({
|
||||||
username,
|
username,
|
||||||
points,
|
points,
|
||||||
@ -51,8 +47,7 @@ const mapStateToProps = createSelector(
|
|||||||
toast,
|
toast,
|
||||||
isNavDropdownOpen,
|
isNavDropdownOpen,
|
||||||
showLoading: !isSignInAttempted,
|
showLoading: !isSignInAttempted,
|
||||||
isSignedIn: !!username,
|
isSignedIn: !!username
|
||||||
isYoutubeLoaded: Object.keys(youtube).length > 0
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -60,10 +55,8 @@ const propTypes = {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
closeDropdown: PropTypes.func.isRequired,
|
closeDropdown: PropTypes.func.isRequired,
|
||||||
fetchUser: PropTypes.func,
|
fetchUser: PropTypes.func,
|
||||||
fetchYoutube: PropTypes.func.isRequired,
|
|
||||||
isNavDropdownOpen: PropTypes.bool,
|
isNavDropdownOpen: PropTypes.bool,
|
||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
isYoutubeLoaded: PropTypes.bool,
|
|
||||||
loadCurrentChallenge: PropTypes.func.isRequired,
|
loadCurrentChallenge: PropTypes.func.isRequired,
|
||||||
openDropdown: PropTypes.func.isRequired,
|
openDropdown: PropTypes.func.isRequired,
|
||||||
params: PropTypes.object,
|
params: PropTypes.object,
|
||||||
@ -89,9 +82,6 @@ export class FreeCodeCamp extends React.Component {
|
|||||||
if (!this.props.isSignedIn) {
|
if (!this.props.isSignedIn) {
|
||||||
this.props.fetchUser();
|
this.props.fetchUser();
|
||||||
}
|
}
|
||||||
if (!this.props.isYoutubeLoaded) {
|
|
||||||
this.props.fetchYoutube();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderChallengeComplete() {
|
renderChallengeComplete() {
|
||||||
|
@ -145,9 +145,3 @@ export const addThemeToBody = createAction(types.addThemeToBody);
|
|||||||
|
|
||||||
export const openDropdown = createAction(types.openDropdown, noop);
|
export const openDropdown = createAction(types.openDropdown, noop);
|
||||||
export const closeDropdown = createAction(types.closeDropdown, noop);
|
export const closeDropdown = createAction(types.closeDropdown, noop);
|
||||||
|
|
||||||
export const fetchYoutube = createAction(types.fetchYoutube);
|
|
||||||
export const updateYoutube = createAction(types.updateYoutube);
|
|
||||||
|
|
||||||
export const updateBlock = createAction(types.updateBlock);
|
|
||||||
export const updateSuperBlock = createAction(types.updateSuperBlock);
|
|
||||||
|
@ -6,32 +6,9 @@ const initialState = {
|
|||||||
superBlock: {},
|
superBlock: {},
|
||||||
block: {},
|
block: {},
|
||||||
challenge: {},
|
challenge: {},
|
||||||
user: {},
|
user: {}
|
||||||
youtube: {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const blocksReducer = handleActions(
|
|
||||||
{
|
|
||||||
[types.updateBlock]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
block: payload
|
|
||||||
}),
|
|
||||||
[types.updateSuperBlock]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
superBlock: payload
|
|
||||||
})
|
|
||||||
}, initialState
|
|
||||||
);
|
|
||||||
|
|
||||||
const youtubeReducer = handleActions(
|
|
||||||
{
|
|
||||||
[types.updateYoutube]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
...payload
|
|
||||||
})
|
|
||||||
}, initialState.youtube
|
|
||||||
);
|
|
||||||
|
|
||||||
const userReducer = handleActions(
|
const userReducer = handleActions(
|
||||||
{
|
{
|
||||||
[types.updateUserPoints]: (state, { payload: { username, points } }) => ({
|
[types.updateUserPoints]: (state, { payload: { username, points } }) => ({
|
||||||
@ -114,19 +91,8 @@ function metaReducer(state = initialState, action) {
|
|||||||
export default function entitiesReducer(state, action) {
|
export default function entitiesReducer(state, action) {
|
||||||
const newState = metaReducer(state, action);
|
const newState = metaReducer(state, action);
|
||||||
const user = userReducer(newState.user, action);
|
const user = userReducer(newState.user, action);
|
||||||
const youtube = youtubeReducer(newState.youtube, action);
|
|
||||||
const blocks = blocksReducer(newState, action);
|
|
||||||
if (newState.user !== user) {
|
if (newState.user !== user) {
|
||||||
return { ...newState, user };
|
return { ...newState, user };
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
newState.block !== blocks.block ||
|
|
||||||
newState.superBlock !== blocks.superBlock
|
|
||||||
) {
|
|
||||||
return { ...newState, ...blocks };
|
|
||||||
}
|
|
||||||
if (newState.youtube !== youtube) {
|
|
||||||
return { ...newState, youtube };
|
|
||||||
}
|
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import { Observable } from 'rx';
|
|
||||||
|
|
||||||
import { get$ } from '../../utils/ajax-stream';
|
|
||||||
import types from './types';
|
|
||||||
import {
|
|
||||||
createErrorObservable,
|
|
||||||
updateBlock,
|
|
||||||
updateSuperBlock,
|
|
||||||
updateYoutube
|
|
||||||
} from './actions';
|
|
||||||
import {
|
|
||||||
initMap,
|
|
||||||
updateSuperBlocks
|
|
||||||
} from '../routes/challenges/redux/actions';
|
|
||||||
import {
|
|
||||||
createMapUi,
|
|
||||||
searchableChallengeTitles
|
|
||||||
} from '../routes/challenges/utils';
|
|
||||||
|
|
||||||
const { fetchYoutube } = types;
|
|
||||||
export default function fetchYoutubeSaga(action$, getState) {
|
|
||||||
return action$
|
|
||||||
.filter(action => action.type === fetchYoutube)
|
|
||||||
.flatMap(() => {
|
|
||||||
const url = '/api/youtube';
|
|
||||||
return get$(url)
|
|
||||||
// allow fetchChallenges to complete
|
|
||||||
.delay(1000)
|
|
||||||
.flatMap(result => {
|
|
||||||
const { youtube } = JSON.parse(result.response);
|
|
||||||
const store = getState();
|
|
||||||
const { entities } = store;
|
|
||||||
const { block, superBlock } = entities;
|
|
||||||
const superBlockWithYoutube = {
|
|
||||||
...superBlock,
|
|
||||||
youtube: {
|
|
||||||
blocks: Object.keys(youtube),
|
|
||||||
dashedName: 'youtube',
|
|
||||||
name: 'YouTube',
|
|
||||||
order: 9,
|
|
||||||
title: 'YouTube'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const youtubeBlocks = Object.keys(youtube)
|
|
||||||
.map(playlist => youtube[playlist])
|
|
||||||
.reduce((accu, current) => {
|
|
||||||
const videosForPlaylist = Object.keys(current.videos)
|
|
||||||
.map(video => current.videos[video].dashedName);
|
|
||||||
return {
|
|
||||||
...accu,
|
|
||||||
[current.dashedName]: {
|
|
||||||
challenges: videosForPlaylist
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
const blockWithYoutube = {
|
|
||||||
...block,
|
|
||||||
...youtubeBlocks
|
|
||||||
};
|
|
||||||
const updatedEntities = {
|
|
||||||
...entities,
|
|
||||||
block: blockWithYoutube,
|
|
||||||
superBlock: superBlockWithYoutube
|
|
||||||
};
|
|
||||||
return Observable.of(
|
|
||||||
updateBlock(blockWithYoutube),
|
|
||||||
// update entities.superBlock
|
|
||||||
updateSuperBlock(superBlockWithYoutube),
|
|
||||||
// update challengesApp.superblocks
|
|
||||||
updateSuperBlocks(Object.keys(superBlockWithYoutube)),
|
|
||||||
updateYoutube(youtube),
|
|
||||||
initMap(
|
|
||||||
createMapUi(
|
|
||||||
updatedEntities,
|
|
||||||
Object.keys(superBlockWithYoutube),
|
|
||||||
searchableChallengeTitles(updatedEntities)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(createErrorObservable);
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,12 +1,10 @@
|
|||||||
import fetchUserSaga from './fetch-user-saga';
|
import fetchUserSaga from './fetch-user-saga';
|
||||||
import loadCurrentChallengeSaga from './load-current-challenge-saga';
|
import loadCurrentChallengeSaga from './load-current-challenge-saga';
|
||||||
import fetchYoutubeSaga from './fetch-youtube-saga';
|
|
||||||
|
|
||||||
export { default as reducer } from './reducer';
|
export { default as reducer } from './reducer';
|
||||||
export * as actions from './actions';
|
export * as actions from './actions';
|
||||||
export { default as types } from './types';
|
export { default as types } from './types';
|
||||||
export const sagas = [
|
export const sagas = [
|
||||||
fetchUserSaga,
|
fetchUserSaga,
|
||||||
loadCurrentChallengeSaga,
|
loadCurrentChallengeSaga
|
||||||
fetchYoutubeSaga
|
|
||||||
];
|
];
|
||||||
|
@ -17,12 +17,6 @@ export default createTypes([
|
|||||||
'loadCurrentChallenge',
|
'loadCurrentChallenge',
|
||||||
'updateMyCurrentChallenge',
|
'updateMyCurrentChallenge',
|
||||||
|
|
||||||
'fetchYoutube',
|
|
||||||
'updateYoutube',
|
|
||||||
|
|
||||||
'updateBlock',
|
|
||||||
'updateSuperBlock',
|
|
||||||
|
|
||||||
'handleError',
|
'handleError',
|
||||||
// used to hit the server
|
// used to hit the server
|
||||||
'hardGoTo',
|
'hardGoTo',
|
||||||
|
@ -26,7 +26,6 @@ export const fetchChallengeCompleted = createAction(
|
|||||||
(_, challenge) => challenge,
|
(_, challenge) => challenge,
|
||||||
entities => ({ entities })
|
entities => ({ entities })
|
||||||
);
|
);
|
||||||
export const updateSuperBlocks = createAction(types.updateSuperBlocks);
|
|
||||||
export const closeChallengeModal = createAction(types.closeChallengeModal);
|
export const closeChallengeModal = createAction(types.closeChallengeModal);
|
||||||
export const resetUi = createAction(types.resetUi);
|
export const resetUi = createAction(types.resetUi);
|
||||||
export const updateHint = createAction(types.updateHint);
|
export const updateHint = createAction(types.updateHint);
|
||||||
|
@ -134,11 +134,7 @@ const mainReducer = handleActions(
|
|||||||
}),
|
}),
|
||||||
[types.fetchChallengesCompleted]: (state, { payload = [] }) => ({
|
[types.fetchChallengesCompleted]: (state, { payload = [] }) => ({
|
||||||
...state,
|
...state,
|
||||||
superBlocks: [ ...payload ]
|
superBlocks: payload
|
||||||
}),
|
|
||||||
[types.updateSuperBlocks]: (state, { payload = [] }) => ({
|
|
||||||
...state,
|
|
||||||
superBlocks: [ ...payload ]
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// step
|
// step
|
||||||
|
@ -24,7 +24,6 @@ export default createTypes([
|
|||||||
'unlockUntrustedCode',
|
'unlockUntrustedCode',
|
||||||
'closeChallengeModal',
|
'closeChallengeModal',
|
||||||
'updateSuccessMessage',
|
'updateSuccessMessage',
|
||||||
'updateSuperBlocks',
|
|
||||||
|
|
||||||
// map
|
// map
|
||||||
'updateFilter',
|
'updateFilter',
|
||||||
|
@ -506,7 +506,7 @@ export function applyFilterToMap(tree, filterRegex) {
|
|||||||
// if leaf (challenge) then test if regex is a match
|
// if leaf (challenge) then test if regex is a match
|
||||||
if (!Array.isArray(node.children)) {
|
if (!Array.isArray(node.children)) {
|
||||||
// does challenge name meet filter criteria?
|
// does challenge name meet filter criteria?
|
||||||
if (filterRegex.test(node.title) || filterRegex.test(node.name)) {
|
if (filterRegex.test(node.title)) {
|
||||||
// is challenge currently hidden?
|
// is challenge currently hidden?
|
||||||
if (node.isHidden) {
|
if (node.isHidden) {
|
||||||
// unhide challenge, it matches
|
// unhide challenge, it matches
|
||||||
|
@ -39,10 +39,6 @@ export class Header extends PureComponent {
|
|||||||
this.handleClearButton = this.handleClearButton.bind(this);
|
this.handleClearButton = this.handleClearButton.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.clearFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
if (e.keyCode === ESC) {
|
if (e.keyCode === ESC) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -7,7 +7,6 @@ import { Col, Row } from 'react-bootstrap';
|
|||||||
|
|
||||||
import MapHeader from './Header.jsx';
|
import MapHeader from './Header.jsx';
|
||||||
import SuperBlock from './Super-Block.jsx';
|
import SuperBlock from './Super-Block.jsx';
|
||||||
import YoutubeSuperBlock from './youtube/YoutubeSuperBlock.jsx';
|
|
||||||
import { fetchChallenges } from '../../redux/actions';
|
import { fetchChallenges } from '../../redux/actions';
|
||||||
import { updateTitle } from '../../../../redux/actions';
|
import { updateTitle } from '../../../../redux/actions';
|
||||||
|
|
||||||
@ -23,12 +22,12 @@ const fetchOptions = {
|
|||||||
};
|
};
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
fetchChallenges: PropTypes.func.isRequired,
|
fetchChallenges: PropTypes.func.isRequired,
|
||||||
isYoutubeLoaded: PropTypes.bool,
|
|
||||||
params: PropTypes.object,
|
params: PropTypes.object,
|
||||||
superBlocks: PropTypes.array,
|
superBlocks: PropTypes.array,
|
||||||
updateTitle: PropTypes.func.isRequired
|
updateTitle: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
class ShowMap extends PureComponent {
|
|
||||||
|
export class ShowMap extends PureComponent {
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
// if no params then map is open in drawer
|
// if no params then map is open in drawer
|
||||||
// do not update title
|
// do not update title
|
||||||
@ -44,23 +43,12 @@ class ShowMap extends PureComponent {
|
|||||||
if (!Array.isArray(superBlocks) || !superBlocks.length) {
|
if (!Array.isArray(superBlocks) || !superBlocks.length) {
|
||||||
return <div>No Super Blocks</div>;
|
return <div>No Super Blocks</div>;
|
||||||
}
|
}
|
||||||
return superBlocks.map(dashedName => {
|
return superBlocks.map(dashedName => (
|
||||||
if (dashedName === 'youtube') {
|
<SuperBlock
|
||||||
return (
|
dashedName={ dashedName }
|
||||||
<YoutubeSuperBlock
|
key={ dashedName }
|
||||||
dashedName={ dashedName }
|
/>
|
||||||
key={ dashedName }
|
));
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<SuperBlock
|
|
||||||
dashedName={ dashedName }
|
|
||||||
key={ dashedName }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import FA from 'react-fontawesome';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import { Panel } from 'react-bootstrap';
|
|
||||||
|
|
||||||
import YoutubeVideo from './YoutubeVideo.jsx';
|
|
||||||
|
|
||||||
import { toggleThisPanel } from '../../../redux/actions';
|
|
||||||
import {
|
|
||||||
makePanelOpenSelector,
|
|
||||||
makePanelHiddenSelector
|
|
||||||
} from '../../../redux/selectors';
|
|
||||||
|
|
||||||
const mapStateToProps = () => createSelector(
|
|
||||||
makePanelOpenSelector(),
|
|
||||||
makePanelHiddenSelector(),
|
|
||||||
(isOpen, isHidden) => {
|
|
||||||
return {
|
|
||||||
isOpen,
|
|
||||||
isHidden
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
toggleThisPanel
|
|
||||||
};
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
dashedName: PropTypes.string,
|
|
||||||
isHidden: PropTypes.bool,
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
title: PropTypes.string,
|
|
||||||
toggleThisPanel: PropTypes.func.isRequired,
|
|
||||||
videos: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export class YoutubeBlock extends PureComponent {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
this.handleSelect = this.handleSelect.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSelect(eventKey, e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.toggleThisPanel(eventKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHeader(isOpen, title) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<FA
|
|
||||||
className='no-link-underline'
|
|
||||||
name={ isOpen ? 'caret-down' : 'caret-right' }
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{ title }
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderVideos(options) {
|
|
||||||
const { videos, dashedName: block } = options;
|
|
||||||
const videosArray = Object.keys(videos).map(video => videos[video]);
|
|
||||||
if (!videosArray.length) {
|
|
||||||
return (<p>No videos found for this playlist</p>);
|
|
||||||
}
|
|
||||||
return videosArray
|
|
||||||
.map((video, i) => (
|
|
||||||
<YoutubeVideo
|
|
||||||
block={ block }
|
|
||||||
dashedName={ video.dashedName }
|
|
||||||
key={ video.dashedName + i }
|
|
||||||
title={ video.title }
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dashedName,
|
|
||||||
isOpen,
|
|
||||||
isHidden,
|
|
||||||
title,
|
|
||||||
videos
|
|
||||||
} = this.props;
|
|
||||||
if (isHidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Panel
|
|
||||||
bsClass='map-accordion-panel-nested'
|
|
||||||
collapsible={ true }
|
|
||||||
eventKey={ dashedName || title }
|
|
||||||
expanded={ isOpen }
|
|
||||||
header={ this.renderHeader(isOpen, title) }
|
|
||||||
id={ title }
|
|
||||||
key={ title }
|
|
||||||
onSelect={ this.handleSelect }
|
|
||||||
>
|
|
||||||
{ this.renderVideos({ videos, dashedName }) }
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
YoutubeBlock.displayName = 'YoutubeBlock';
|
|
||||||
YoutubeBlock.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(YoutubeBlock);
|
|
@ -1,117 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import FA from 'react-fontawesome';
|
|
||||||
import { Panel } from 'react-bootstrap';
|
|
||||||
|
|
||||||
import YoutubeBlock from './YoutubeBlock.jsx';
|
|
||||||
|
|
||||||
import { toggleThisPanel } from '../../../redux/actions';
|
|
||||||
import {
|
|
||||||
makePanelOpenSelector,
|
|
||||||
makePanelHiddenSelector
|
|
||||||
} from '../../../redux/selectors';
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
toggleThisPanel
|
|
||||||
};
|
|
||||||
const mapStateToProps = () => {
|
|
||||||
const panelOpenSelector = makePanelOpenSelector();
|
|
||||||
const panelHiddenSelector = makePanelHiddenSelector();
|
|
||||||
return createSelector(
|
|
||||||
(_, props) => props.dashedName,
|
|
||||||
state => state.entities.youtube,
|
|
||||||
panelOpenSelector,
|
|
||||||
panelHiddenSelector,
|
|
||||||
(dashedName, youtube, isOpen, isHidden) => ({
|
|
||||||
dashedName,
|
|
||||||
isOpen,
|
|
||||||
isHidden,
|
|
||||||
youtube
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const propTypes = {
|
|
||||||
dashedName: PropTypes.string,
|
|
||||||
isHidden: PropTypes.bool,
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
toggleThisPanel: PropTypes.func.isRequired,
|
|
||||||
youtube: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export class YoutubeSuperBlock extends PureComponent {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
this.handleSelect = this.handleSelect.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSelect(eventKey, e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.toggleThisPanel(eventKey.slice(0).toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHeader(isOpen, title) {
|
|
||||||
return (
|
|
||||||
<h2>
|
|
||||||
<FA
|
|
||||||
className='no-link-underline'
|
|
||||||
name={ isOpen ? 'caret-down' : 'caret-right' }
|
|
||||||
/>
|
|
||||||
{ title }
|
|
||||||
</h2>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPlaylists(playlists) {
|
|
||||||
return Object.keys(playlists).map((playlist) => {
|
|
||||||
const { dashedName, title, videos } = playlists[playlist];
|
|
||||||
return (
|
|
||||||
<YoutubeBlock
|
|
||||||
dashedName={ dashedName }
|
|
||||||
key={ dashedName }
|
|
||||||
title= { title }
|
|
||||||
videos={ videos }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
isHidden,
|
|
||||||
youtube
|
|
||||||
} = this.props;
|
|
||||||
const title = 'YouTube';
|
|
||||||
if (isHidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Panel
|
|
||||||
bsClass='map-accordion-panel'
|
|
||||||
collapsible={ true }
|
|
||||||
eventKey={ title }
|
|
||||||
expanded={ isOpen }
|
|
||||||
header={ this.renderHeader(isOpen, title) }
|
|
||||||
id={ title }
|
|
||||||
key={ title }
|
|
||||||
onSelect={ this.handleSelect }
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className='map-accordion-block'
|
|
||||||
>
|
|
||||||
{ this.renderPlaylists(youtube) }
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
YoutubeSuperBlock.dsiplayName = 'YoutubeSuperBlock';
|
|
||||||
YoutubeSuperBlock.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(YoutubeSuperBlock);
|
|
@ -1,62 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
import { makePanelHiddenSelector } from '../../../redux/selectors';
|
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
|
||||||
|
|
||||||
const mapStateToProps = () => createSelector(
|
|
||||||
makePanelHiddenSelector(),
|
|
||||||
( isHidden ) => ({ isHidden })
|
|
||||||
);
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
block: PropTypes.string,
|
|
||||||
dashedName: PropTypes.string,
|
|
||||||
isHidden: PropTypes.bool,
|
|
||||||
title: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export class YoutubeVideo extends PureComponent {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
block: playlist,
|
|
||||||
dashedName: video,
|
|
||||||
isHidden,
|
|
||||||
title
|
|
||||||
} = this.props;
|
|
||||||
const challengeClassName = classnames({
|
|
||||||
'text-primary': true,
|
|
||||||
'padded-ionic-icon': true,
|
|
||||||
'negative-15': true,
|
|
||||||
'challenge-title': true
|
|
||||||
});
|
|
||||||
if (isHidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
className={ challengeClassName }
|
|
||||||
key={ title }
|
|
||||||
>
|
|
||||||
<Link to={ `/youtube/${playlist}/${video}` }>
|
|
||||||
<span>
|
|
||||||
{ title }
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
YoutubeVideo.displayName = 'YoutubeVideo';
|
|
||||||
YoutubeVideo.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(YoutubeVideo);
|
|
@ -6,7 +6,6 @@ import {
|
|||||||
import NotFound from '../components/NotFound/index.jsx';
|
import NotFound from '../components/NotFound/index.jsx';
|
||||||
import { addLang } from '../utils/lang';
|
import { addLang } from '../utils/lang';
|
||||||
import settingsRoute from './settings';
|
import settingsRoute from './settings';
|
||||||
import youtubeRoute from './youtube';
|
|
||||||
|
|
||||||
export default function createChildRoute(deps) {
|
export default function createChildRoute(deps) {
|
||||||
return {
|
return {
|
||||||
@ -23,7 +22,6 @@ export default function createChildRoute(deps) {
|
|||||||
modernChallengesRoute(deps),
|
modernChallengesRoute(deps),
|
||||||
mapRoute(deps),
|
mapRoute(deps),
|
||||||
settingsRoute(deps),
|
settingsRoute(deps),
|
||||||
youtubeRoute(deps),
|
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
component: NotFound
|
component: NotFound
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import { Col } from 'react-bootstrap';
|
|
||||||
import YouTube from 'react-youtube';
|
|
||||||
|
|
||||||
import { updateTitle } from '../../../redux/actions';
|
|
||||||
|
|
||||||
import * as styles from '../styles';
|
|
||||||
|
|
||||||
const mapActionsToDispatch = {
|
|
||||||
updateTitle
|
|
||||||
};
|
|
||||||
const mapStateToProps = createSelector(
|
|
||||||
(_, props) => props.params.playlist,
|
|
||||||
(_, props) => props.params.video,
|
|
||||||
state => state.entities.youtube,
|
|
||||||
(selectedPlaylist, selectedVideo, youtube = {}) => {
|
|
||||||
const playlist = youtube[selectedPlaylist];
|
|
||||||
return {
|
|
||||||
playlist,
|
|
||||||
video: playlist.videos[selectedVideo]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
playlist: PropTypes.object,
|
|
||||||
updateTitle: PropTypes.func.isRequired,
|
|
||||||
video: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
class YoutubeVideo extends PureComponent {
|
|
||||||
componentWillMount() {
|
|
||||||
const { updateTitle, playlist, video } = this.props;
|
|
||||||
const title = playlist.title && video.title ?
|
|
||||||
`${playlist.title} - ${video.title}` :
|
|
||||||
'freeCodeCamp on YouTube';
|
|
||||||
updateTitle(title);
|
|
||||||
}
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
const { updateTitle, playlist, video } = nextProps;
|
|
||||||
if (this.props.video !== video) {
|
|
||||||
updateTitle(`${playlist.title} - ${video.title}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
playlist: { title: playlistTitle },
|
|
||||||
video: {
|
|
||||||
description,
|
|
||||||
title: videoTitle,
|
|
||||||
videoId
|
|
||||||
}
|
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
descriptionContainer,
|
|
||||||
descriptionText
|
|
||||||
} = styles;
|
|
||||||
const options = {
|
|
||||||
width: '100%',
|
|
||||||
playerVars: { autoplay: 1 }
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div >
|
|
||||||
<Col md={8} mdOffset={2} xs={12}>
|
|
||||||
<h2>
|
|
||||||
{ `${playlistTitle} - ${videoTitle}` }
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<YouTube
|
|
||||||
opts={ options }
|
|
||||||
videoId={ videoId }
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={ descriptionContainer }>
|
|
||||||
<p style={ descriptionText }>
|
|
||||||
{ description }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
YoutubeVideo.displayName = 'YoutubeVideo';
|
|
||||||
YoutubeVideo.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapActionsToDispatch)(YoutubeVideo);
|
|
@ -1,8 +0,0 @@
|
|||||||
import YoutubeVideo from './components/YoutubeVideo.jsx';
|
|
||||||
|
|
||||||
export default function youtubeVideoRoute() {
|
|
||||||
return {
|
|
||||||
path: 'youtube/:playlist/:video',
|
|
||||||
component: YoutubeVideo
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
export const descriptionContainer = {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const descriptionText = {
|
|
||||||
width: '90%'
|
|
||||||
};
|
|
@ -1,8 +1,6 @@
|
|||||||
import request from 'request';
|
import request from 'request';
|
||||||
|
|
||||||
import constantStrings from '../utils/constantStrings.json';
|
import constantStrings from '../utils/constantStrings.json';
|
||||||
import testimonials from '../resources/testimonials.json';
|
import testimonials from '../resources/testimonials.json';
|
||||||
import { serveYoutubeApiResponse } from '../openApi';
|
|
||||||
|
|
||||||
const githubClient = process.env.GITHUB_ID;
|
const githubClient = process.env.GITHUB_ID;
|
||||||
const githubSecret = process.env.GITHUB_SECRET;
|
const githubSecret = process.env.GITHUB_SECRET;
|
||||||
@ -12,7 +10,6 @@ module.exports = function(app) {
|
|||||||
const User = app.models.User;
|
const User = app.models.User;
|
||||||
const noLangRouter = app.loopback.Router();
|
const noLangRouter = app.loopback.Router();
|
||||||
noLangRouter.get('/api/github', githubCalls);
|
noLangRouter.get('/api/github', githubCalls);
|
||||||
noLangRouter.get('/api/youtube', serveYoutubeApiResponse);
|
|
||||||
noLangRouter.get('/chat', chat);
|
noLangRouter.get('/chat', chat);
|
||||||
noLangRouter.get('/twitch', twitch);
|
noLangRouter.get('/twitch', twitch);
|
||||||
noLangRouter.get('/unsubscribe/:email', unsubscribeAll);
|
noLangRouter.get('/unsubscribe/:email', unsubscribeAll);
|
||||||
|
3
server/boot/react.js
vendored
3
server/boot/react.js
vendored
@ -30,9 +30,6 @@ export default function reactSubRouter(app) {
|
|||||||
(req, res) => res.redirect(`/challenges/${req.params.dashedName}`)
|
(req, res) => res.redirect(`/challenges/${req.params.dashedName}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
// youtube data not available on app load, send to the map
|
|
||||||
router.get(/youtube.*/, (req, res) => res.redirect('/map'));
|
|
||||||
|
|
||||||
// These routes are in production
|
// These routes are in production
|
||||||
routes.forEach((route) => {
|
routes.forEach((route) => {
|
||||||
router.get(route, serveReactApp);
|
router.get(route, serveReactApp);
|
||||||
|
@ -1,119 +0,0 @@
|
|||||||
import { Observable } from 'rx';
|
|
||||||
import debug from 'debug';
|
|
||||||
|
|
||||||
import request from 'request';
|
|
||||||
import { dasherize } from '../utils';
|
|
||||||
|
|
||||||
const log = debug('fcc:openApi');
|
|
||||||
|
|
||||||
let dbTimestamp, cachedYoutubeResponse;
|
|
||||||
|
|
||||||
function apiCall(uri) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
request(uri, (err, res, body) => {
|
|
||||||
if (err) { reject(err); }
|
|
||||||
resolve(body);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByDashedName(a, b) {
|
|
||||||
if (a.dashedName < b.dashedName) { return -1; }
|
|
||||||
if (a.dashedName > b.dashedName) { return 1; }
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const source = Observable.timer(200000, 3600000)
|
|
||||||
.selectMany(() => {
|
|
||||||
const today = new Date().getDay();
|
|
||||||
if (dbTimestamp !== today) {
|
|
||||||
hydrateCache();
|
|
||||||
return Observable.of(log('Updating cache'));
|
|
||||||
}
|
|
||||||
return Observable.of(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
function hydrateCache() {
|
|
||||||
log('requesting data from openApi');
|
|
||||||
Observable.fromPromise(
|
|
||||||
apiCall('https://glitter-organisation.gomix.me/api/v1/all')
|
|
||||||
)
|
|
||||||
.subscribe(
|
|
||||||
result => {
|
|
||||||
const {
|
|
||||||
lastUpdated,
|
|
||||||
youtube: { playlists, videos }
|
|
||||||
} = JSON.parse(result);
|
|
||||||
dbTimestamp = lastUpdated;
|
|
||||||
const youtubeVideos = videos
|
|
||||||
.map(video => {
|
|
||||||
const {
|
|
||||||
snippet: {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
playlistId,
|
|
||||||
resourceId: { videoId }
|
|
||||||
}
|
|
||||||
} = video;
|
|
||||||
|
|
||||||
return {
|
|
||||||
description,
|
|
||||||
playlistId,
|
|
||||||
title,
|
|
||||||
videoId,
|
|
||||||
dashedName: dasherize(title)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const youtube = playlists
|
|
||||||
.map(list => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
snippet: { title }
|
|
||||||
} = list;
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
dashedName: dasherize(title)
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort(sortByDashedName)
|
|
||||||
.reduce((accu, current) => {
|
|
||||||
const videosForCurrent = youtubeVideos
|
|
||||||
.filter(video => video.playlistId === current.id)
|
|
||||||
.sort(sortByDashedName)
|
|
||||||
.reduce((accu, current) => ({
|
|
||||||
...accu,
|
|
||||||
[current.dashedName]: { ...current }
|
|
||||||
}), {});
|
|
||||||
return {
|
|
||||||
...accu,
|
|
||||||
[current.dashedName]: {
|
|
||||||
...current,
|
|
||||||
videos: videosForCurrent
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
cachedYoutubeResponse = { youtube };
|
|
||||||
log('finished updating cache');
|
|
||||||
},
|
|
||||||
err => log(err)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function startSubscription() {
|
|
||||||
log('Hydrating cache');
|
|
||||||
hydrateCache();
|
|
||||||
source.subscribe(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serveYoutubeApiResponse(req, res) {
|
|
||||||
if (cachedYoutubeResponse) {
|
|
||||||
res.json(cachedYoutubeResponse);
|
|
||||||
} else {
|
|
||||||
res.status(500).json(
|
|
||||||
{
|
|
||||||
error: 'Something went wrong at our end, please try again.'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,8 +9,7 @@ var _ = require('lodash'),
|
|||||||
boot = require('loopback-boot'),
|
boot = require('loopback-boot'),
|
||||||
expressState = require('express-state'),
|
expressState = require('express-state'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
setupPassport = require('./component-passport'),
|
setupPassport = require('./component-passport');
|
||||||
openApi = require('./openApi');
|
|
||||||
|
|
||||||
// polyfill for webpack bundle splitting
|
// polyfill for webpack bundle splitting
|
||||||
const requireProto = Object.getPrototypeOf(require);
|
const requireProto = Object.getPrototypeOf(require);
|
||||||
@ -50,7 +49,6 @@ setupPassport(app);
|
|||||||
app.start = _.once(function() {
|
app.start = _.once(function() {
|
||||||
app.listen(app.get('port'), function() {
|
app.listen(app.get('port'), function() {
|
||||||
app.emit('started');
|
app.emit('started');
|
||||||
openApi.startSubscription();
|
|
||||||
console.log(
|
console.log(
|
||||||
'freeCodeCamp server listening on port %d in %s',
|
'freeCodeCamp server listening on port %d in %s',
|
||||||
app.get('port'),
|
app.get('port'),
|
||||||
|
Reference in New Issue
Block a user