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 {
|
||||
fetchUser,
|
||||
fetchYoutube,
|
||||
updateAppLang,
|
||||
trackEvent,
|
||||
loadCurrentChallenge,
|
||||
@ -23,7 +22,6 @@ import { userSelector } from './redux/selectors';
|
||||
const mapDispatchToProps = {
|
||||
closeDropdown,
|
||||
fetchUser,
|
||||
fetchYoutube,
|
||||
loadCurrentChallenge,
|
||||
openDropdown,
|
||||
submitChallenge,
|
||||
@ -37,13 +35,11 @@ const mapStateToProps = createSelector(
|
||||
state => state.app.isSignInAttempted,
|
||||
state => state.app.toast,
|
||||
state => state.challengesApp.toast,
|
||||
state => state.entities.youtube,
|
||||
(
|
||||
{ user: { username, points, picture } },
|
||||
isNavDropdownOpen,
|
||||
isSignInAttempted,
|
||||
toast,
|
||||
youtube
|
||||
) => ({
|
||||
username,
|
||||
points,
|
||||
@ -51,8 +47,7 @@ const mapStateToProps = createSelector(
|
||||
toast,
|
||||
isNavDropdownOpen,
|
||||
showLoading: !isSignInAttempted,
|
||||
isSignedIn: !!username,
|
||||
isYoutubeLoaded: Object.keys(youtube).length > 0
|
||||
isSignedIn: !!username
|
||||
})
|
||||
);
|
||||
|
||||
@ -60,10 +55,8 @@ const propTypes = {
|
||||
children: PropTypes.node,
|
||||
closeDropdown: PropTypes.func.isRequired,
|
||||
fetchUser: PropTypes.func,
|
||||
fetchYoutube: PropTypes.func.isRequired,
|
||||
isNavDropdownOpen: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool,
|
||||
isYoutubeLoaded: PropTypes.bool,
|
||||
loadCurrentChallenge: PropTypes.func.isRequired,
|
||||
openDropdown: PropTypes.func.isRequired,
|
||||
params: PropTypes.object,
|
||||
@ -89,9 +82,6 @@ export class FreeCodeCamp extends React.Component {
|
||||
if (!this.props.isSignedIn) {
|
||||
this.props.fetchUser();
|
||||
}
|
||||
if (!this.props.isYoutubeLoaded) {
|
||||
this.props.fetchYoutube();
|
||||
}
|
||||
}
|
||||
|
||||
renderChallengeComplete() {
|
||||
|
@ -145,9 +145,3 @@ export const addThemeToBody = createAction(types.addThemeToBody);
|
||||
|
||||
export const openDropdown = createAction(types.openDropdown, 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: {},
|
||||
block: {},
|
||||
challenge: {},
|
||||
user: {},
|
||||
youtube: {}
|
||||
user: {}
|
||||
};
|
||||
|
||||
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(
|
||||
{
|
||||
[types.updateUserPoints]: (state, { payload: { username, points } }) => ({
|
||||
@ -114,19 +91,8 @@ function metaReducer(state = initialState, action) {
|
||||
export default function entitiesReducer(state, action) {
|
||||
const newState = metaReducer(state, action);
|
||||
const user = userReducer(newState.user, action);
|
||||
const youtube = youtubeReducer(newState.youtube, action);
|
||||
const blocks = blocksReducer(newState, action);
|
||||
if (newState.user !== 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;
|
||||
}
|
||||
|
@ -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 loadCurrentChallengeSaga from './load-current-challenge-saga';
|
||||
import fetchYoutubeSaga from './fetch-youtube-saga';
|
||||
|
||||
export { default as reducer } from './reducer';
|
||||
export * as actions from './actions';
|
||||
export { default as types } from './types';
|
||||
export const sagas = [
|
||||
fetchUserSaga,
|
||||
loadCurrentChallengeSaga,
|
||||
fetchYoutubeSaga
|
||||
loadCurrentChallengeSaga
|
||||
];
|
||||
|
@ -17,12 +17,6 @@ export default createTypes([
|
||||
'loadCurrentChallenge',
|
||||
'updateMyCurrentChallenge',
|
||||
|
||||
'fetchYoutube',
|
||||
'updateYoutube',
|
||||
|
||||
'updateBlock',
|
||||
'updateSuperBlock',
|
||||
|
||||
'handleError',
|
||||
// used to hit the server
|
||||
'hardGoTo',
|
||||
|
@ -26,7 +26,6 @@ export const fetchChallengeCompleted = createAction(
|
||||
(_, challenge) => challenge,
|
||||
entities => ({ entities })
|
||||
);
|
||||
export const updateSuperBlocks = createAction(types.updateSuperBlocks);
|
||||
export const closeChallengeModal = createAction(types.closeChallengeModal);
|
||||
export const resetUi = createAction(types.resetUi);
|
||||
export const updateHint = createAction(types.updateHint);
|
||||
|
@ -134,11 +134,7 @@ const mainReducer = handleActions(
|
||||
}),
|
||||
[types.fetchChallengesCompleted]: (state, { payload = [] }) => ({
|
||||
...state,
|
||||
superBlocks: [ ...payload ]
|
||||
}),
|
||||
[types.updateSuperBlocks]: (state, { payload = [] }) => ({
|
||||
...state,
|
||||
superBlocks: [ ...payload ]
|
||||
superBlocks: payload
|
||||
}),
|
||||
|
||||
// step
|
||||
|
@ -24,7 +24,6 @@ export default createTypes([
|
||||
'unlockUntrustedCode',
|
||||
'closeChallengeModal',
|
||||
'updateSuccessMessage',
|
||||
'updateSuperBlocks',
|
||||
|
||||
// map
|
||||
'updateFilter',
|
||||
|
@ -506,7 +506,7 @@ export function applyFilterToMap(tree, filterRegex) {
|
||||
// if leaf (challenge) then test if regex is a match
|
||||
if (!Array.isArray(node.children)) {
|
||||
// does challenge name meet filter criteria?
|
||||
if (filterRegex.test(node.title) || filterRegex.test(node.name)) {
|
||||
if (filterRegex.test(node.title)) {
|
||||
// is challenge currently hidden?
|
||||
if (node.isHidden) {
|
||||
// unhide challenge, it matches
|
||||
|
@ -39,10 +39,6 @@ export class Header extends PureComponent {
|
||||
this.handleClearButton = this.handleClearButton.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearFilter();
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === ESC) {
|
||||
e.preventDefault();
|
||||
|
@ -7,7 +7,6 @@ import { Col, Row } from 'react-bootstrap';
|
||||
|
||||
import MapHeader from './Header.jsx';
|
||||
import SuperBlock from './Super-Block.jsx';
|
||||
import YoutubeSuperBlock from './youtube/YoutubeSuperBlock.jsx';
|
||||
import { fetchChallenges } from '../../redux/actions';
|
||||
import { updateTitle } from '../../../../redux/actions';
|
||||
|
||||
@ -23,12 +22,12 @@ const fetchOptions = {
|
||||
};
|
||||
const propTypes = {
|
||||
fetchChallenges: PropTypes.func.isRequired,
|
||||
isYoutubeLoaded: PropTypes.bool,
|
||||
params: PropTypes.object,
|
||||
superBlocks: PropTypes.array,
|
||||
updateTitle: PropTypes.func.isRequired
|
||||
};
|
||||
class ShowMap extends PureComponent {
|
||||
|
||||
export class ShowMap extends PureComponent {
|
||||
componentWillMount() {
|
||||
// if no params then map is open in drawer
|
||||
// do not update title
|
||||
@ -44,23 +43,12 @@ class ShowMap extends PureComponent {
|
||||
if (!Array.isArray(superBlocks) || !superBlocks.length) {
|
||||
return <div>No Super Blocks</div>;
|
||||
}
|
||||
return superBlocks.map(dashedName => {
|
||||
if (dashedName === 'youtube') {
|
||||
return (
|
||||
<YoutubeSuperBlock
|
||||
dashedName={ dashedName }
|
||||
key={ dashedName }
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SuperBlock
|
||||
dashedName={ dashedName }
|
||||
key={ dashedName }
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
return superBlocks.map(dashedName => (
|
||||
<SuperBlock
|
||||
dashedName={ dashedName }
|
||||
key={ dashedName }
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
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 { addLang } from '../utils/lang';
|
||||
import settingsRoute from './settings';
|
||||
import youtubeRoute from './youtube';
|
||||
|
||||
export default function createChildRoute(deps) {
|
||||
return {
|
||||
@ -23,7 +22,6 @@ export default function createChildRoute(deps) {
|
||||
modernChallengesRoute(deps),
|
||||
mapRoute(deps),
|
||||
settingsRoute(deps),
|
||||
youtubeRoute(deps),
|
||||
{
|
||||
path: '*',
|
||||
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 constantStrings from '../utils/constantStrings.json';
|
||||
import testimonials from '../resources/testimonials.json';
|
||||
import { serveYoutubeApiResponse } from '../openApi';
|
||||
|
||||
const githubClient = process.env.GITHUB_ID;
|
||||
const githubSecret = process.env.GITHUB_SECRET;
|
||||
@ -12,7 +10,6 @@ module.exports = function(app) {
|
||||
const User = app.models.User;
|
||||
const noLangRouter = app.loopback.Router();
|
||||
noLangRouter.get('/api/github', githubCalls);
|
||||
noLangRouter.get('/api/youtube', serveYoutubeApiResponse);
|
||||
noLangRouter.get('/chat', chat);
|
||||
noLangRouter.get('/twitch', twitch);
|
||||
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}`)
|
||||
);
|
||||
|
||||
// youtube data not available on app load, send to the map
|
||||
router.get(/youtube.*/, (req, res) => res.redirect('/map'));
|
||||
|
||||
// These routes are in production
|
||||
routes.forEach((route) => {
|
||||
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'),
|
||||
expressState = require('express-state'),
|
||||
path = require('path'),
|
||||
setupPassport = require('./component-passport'),
|
||||
openApi = require('./openApi');
|
||||
setupPassport = require('./component-passport');
|
||||
|
||||
// polyfill for webpack bundle splitting
|
||||
const requireProto = Object.getPrototypeOf(require);
|
||||
@ -50,7 +49,6 @@ setupPassport(app);
|
||||
app.start = _.once(function() {
|
||||
app.listen(app.get('port'), function() {
|
||||
app.emit('started');
|
||||
openApi.startSubscription();
|
||||
console.log(
|
||||
'freeCodeCamp server listening on port %d in %s',
|
||||
app.get('port'),
|
||||
|
Reference in New Issue
Block a user