Merge pull request #14708 from freeCodeCamp/revert-13592-feature/youtubeInMap

Revert "Add Youtube videos to the map"
This commit is contained in:
Berkeley Martinez
2017-05-03 17:25:08 -07:00
committed by GitHub
23 changed files with 14 additions and 711 deletions

View File

@ -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() {

View File

@ -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);

View File

@ -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;
}

View File

@ -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);
});
}

View File

@ -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
];

View File

@ -17,12 +17,6 @@ export default createTypes([
'loadCurrentChallenge',
'updateMyCurrentChallenge',
'fetchYoutube',
'updateYoutube',
'updateBlock',
'updateSuperBlock',
'handleError',
// used to hit the server
'hardGoTo',

View File

@ -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);

View File

@ -134,11 +134,7 @@ const mainReducer = handleActions(
}),
[types.fetchChallengesCompleted]: (state, { payload = [] }) => ({
...state,
superBlocks: [ ...payload ]
}),
[types.updateSuperBlocks]: (state, { payload = [] }) => ({
...state,
superBlocks: [ ...payload ]
superBlocks: payload
}),
// step

View File

@ -24,7 +24,6 @@ export default createTypes([
'unlockUntrustedCode',
'closeChallengeModal',
'updateSuccessMessage',
'updateSuperBlocks',
// map
'updateFilter',

View File

@ -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

View File

@ -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();

View File

@ -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() {

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -1,8 +0,0 @@
import YoutubeVideo from './components/YoutubeVideo.jsx';
export default function youtubeVideoRoute() {
return {
path: 'youtube/:playlist/:video',
component: YoutubeVideo
};
}

View File

@ -1,8 +0,0 @@
export const descriptionContainer = {
display: 'flex',
justifyContent: 'center'
};
export const descriptionText = {
width: '90%'
};

View File

@ -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);

View File

@ -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);

View File

@ -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.'
}
);
}
}

View File

@ -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'),