Feat: News in the client app (#34392)
This commit is contained in:
@ -1,214 +1,259 @@
|
|||||||
// import React from 'react';
|
import { has, pick, isEmpty } from 'lodash';
|
||||||
// import { renderToString } from 'react-dom/server';
|
|
||||||
// // import { StaticRouter } from 'react-router-dom';
|
|
||||||
// import { has } from 'lodash';
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import { reportError } from '../middlewares/error-reporter';
|
||||||
|
|
||||||
// import NewsApp from '../../news/NewsApp';
|
const log = debug('fcc:boot:news');
|
||||||
|
|
||||||
const routerLog = debug('fcc:boot:news:router');
|
|
||||||
const apiLog = debug('fcc:boot:news:api');
|
|
||||||
|
|
||||||
export default function newsBoot(app) {
|
export default function newsBoot(app) {
|
||||||
// const router = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
// const api = app.loopback.Router();
|
|
||||||
|
|
||||||
// router.get('/n', (req, res) => res.redirect('/news'));
|
api.get('/n/:shortId', createShortLinkHandler(app));
|
||||||
// router.get('/n/:shortId', createShortLinkHandler(app));
|
|
||||||
|
|
||||||
// router.get('/news', serveNewsApp);
|
api.post('/p', createPopularityHandler(app));
|
||||||
// router.get('/news/*', serveNewsApp);
|
|
||||||
|
|
||||||
// api.post('/p', createPopularityHandler(app));
|
app.use('/internal', api);
|
||||||
|
|
||||||
// app.use(api);
|
|
||||||
// app.use(router);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// function serveNewsApp(req, res) {
|
function createShortLinkHandler(app) {
|
||||||
// const context = {};
|
const { Article } = app.models;
|
||||||
// const markup = renderToString(
|
|
||||||
// <StaticRouter basename='/news' context={context} location={req.url}>
|
|
||||||
// <NewsApp />
|
|
||||||
// </StaticRouter>
|
|
||||||
// );
|
|
||||||
// if (context.url) {
|
|
||||||
// routerLog('redirect found in `renderToString`');
|
|
||||||
// // 'client-side' routing hit on a redirect
|
|
||||||
// return res.redirect(context.url);
|
|
||||||
// }
|
|
||||||
// routerLog('news markup sending');
|
|
||||||
// return res.render('layout-news', { title: 'News | freeCodeCamp', markup });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function createShortLinkHandler(app) {
|
const referralHandler = createReferralHandler(app);
|
||||||
// const { Article } = app.models;
|
|
||||||
|
|
||||||
// const referralHandler = createRerralHandler(app);
|
return function shortLinkHandler(req, res, next) {
|
||||||
|
const { query, user } = req;
|
||||||
|
const { shortId } = req.params;
|
||||||
|
|
||||||
// return function shortLinkHandler(req, res, next) {
|
// We manually report the error here as it should not affect this request
|
||||||
// const { query, user } = req;
|
referralHandler(query, shortId, !!user).catch(err => reportError(err));
|
||||||
// const { shortId } = req.params;
|
|
||||||
|
|
||||||
// referralHandler(query, shortId, !!user);
|
if (!shortId) {
|
||||||
|
return res.sendStatus(400);
|
||||||
|
}
|
||||||
|
return Article.findOne(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
or: [{ shortId }, { slugPart: shortId }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err, article) => {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
if (!article) {
|
||||||
|
return res.status(404).send('Could not find article by shortId');
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
slugPart,
|
||||||
|
shortId,
|
||||||
|
author: { username }
|
||||||
|
} = article;
|
||||||
|
const slug = `/news/${username}/${slugPart}--${shortId}`;
|
||||||
|
const articleData = {
|
||||||
|
...pick(article, [
|
||||||
|
'author',
|
||||||
|
'renderableContent',
|
||||||
|
'firstPublishedDate',
|
||||||
|
'viewCount',
|
||||||
|
'title',
|
||||||
|
'featureImage',
|
||||||
|
'slugPart',
|
||||||
|
'shortId',
|
||||||
|
'meta'
|
||||||
|
]),
|
||||||
|
slug
|
||||||
|
};
|
||||||
|
return res.json(articleData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// routerLog(req.origin);
|
function createPopularityHandler(app) {
|
||||||
// routerLog(query.refsource);
|
const { Article, Popularity } = app.models;
|
||||||
// if (!shortId) {
|
|
||||||
// return res.redirect('/news');
|
|
||||||
// }
|
|
||||||
// routerLog('shortId', shortId);
|
|
||||||
// return Article.findOne(
|
|
||||||
// {
|
|
||||||
// where: {
|
|
||||||
// or: [{ shortId }, { slugPart: shortId }]
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// (err, article) => {
|
|
||||||
// if (err) {
|
|
||||||
// next(err);
|
|
||||||
// }
|
|
||||||
// if (!article) {
|
|
||||||
// return res.redirect('/news');
|
|
||||||
// }
|
|
||||||
// const {
|
|
||||||
// slugPart,
|
|
||||||
// shortId,
|
|
||||||
// author: { username }
|
|
||||||
// } = article;
|
|
||||||
// const slug = `/news/${username}/${slugPart}--${shortId}`;
|
|
||||||
// return res.redirect(slug);
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function createPopularityHandler(app) {
|
function findArticleByShortId(shortId) {
|
||||||
// const { Article, Popularity } = app.models;
|
return new Promise((resolve, reject) =>
|
||||||
|
Article.findOne({ where: { shortId } }, (err, article) => {
|
||||||
|
if (err) {
|
||||||
|
log('Error returned from Article.findOne(shortId)');
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
log('article found');
|
||||||
|
return resolve(article);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// return function handlePopularityStats(req, res, next) {
|
function findPopularityByShortId(shortId) {
|
||||||
// const { body, user } = req;
|
return new Promise((resolve, reject) =>
|
||||||
|
Popularity.findOne(
|
||||||
|
{ where: { articleId: shortId } },
|
||||||
|
(err, popularity) => {
|
||||||
|
if (err) {
|
||||||
|
log('Error returned from Popularity.findOne(shortId)');
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
log('popularity found');
|
||||||
|
return resolve(popularity);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// if (
|
function createPopularity(popularityUpdate, shortId) {
|
||||||
// !has(body, 'event') ||
|
return new Promise((resolve, reject) =>
|
||||||
// !has(body, 'timestamp') ||
|
Popularity.create(
|
||||||
// !has(body, 'shortId')
|
{
|
||||||
// ) {
|
events: [popularityUpdate],
|
||||||
// console.warn('Popularity event recieved from client is malformed');
|
articleId: shortId
|
||||||
// console.log(JSON.stringify(body, null, 2));
|
},
|
||||||
// // sending 200 because the client shouldn't care for this
|
err => {
|
||||||
// return res.sendStatus(200);
|
if (err) {
|
||||||
// }
|
return reject(err);
|
||||||
// res.sendStatus(200);
|
}
|
||||||
// const { shortId } = body;
|
log('poulartiy created');
|
||||||
// apiLog('shortId', shortId);
|
return resolve();
|
||||||
// const populartiyUpdate = {
|
}
|
||||||
// ...body,
|
)
|
||||||
// byAuthenticatedUser: !!user
|
);
|
||||||
// };
|
}
|
||||||
// Popularity.findOne({ where: { articleId: shortId } }, (err, popularity) => {
|
|
||||||
// if (err) {
|
|
||||||
// apiLog(err);
|
|
||||||
// return next(err);
|
|
||||||
// }
|
|
||||||
// if (popularity) {
|
|
||||||
// return popularity.updateAttribute(
|
|
||||||
// 'events',
|
|
||||||
// [populartiyUpdate, ...popularity.events],
|
|
||||||
// err => {
|
|
||||||
// if (err) {
|
|
||||||
// apiLog(err);
|
|
||||||
// return next(err);
|
|
||||||
// }
|
|
||||||
// return apiLog('poplarity updated');
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// return Popularity.create(
|
|
||||||
// {
|
|
||||||
// events: [populartiyUpdate],
|
|
||||||
// articleId: shortId
|
|
||||||
// },
|
|
||||||
// err => {
|
|
||||||
// if (err) {
|
|
||||||
// apiLog(err);
|
|
||||||
// return next(err);
|
|
||||||
// }
|
|
||||||
// return apiLog('poulartiy created');
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// return body.event === 'view'
|
|
||||||
// ? Article.findOne({ where: { shortId } }, (err, article) => {
|
|
||||||
// if (err) {
|
|
||||||
// apiLog(err);
|
|
||||||
// next(err);
|
|
||||||
// }
|
|
||||||
// return article.updateAttributes(
|
|
||||||
// { viewCount: article.viewCount + 1 },
|
|
||||||
// err => {
|
|
||||||
// if (err) {
|
|
||||||
// apiLog(err);
|
|
||||||
// return next(err);
|
|
||||||
// }
|
|
||||||
// return apiLog('article views updated');
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
// : null;
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function createRerralHandler(app) {
|
function updatePopularity(popularity, popularityUpdate) {
|
||||||
// const { Popularity } = app.models;
|
return new Promise((resolve, reject) =>
|
||||||
|
popularity.updateAttribute(
|
||||||
|
'events',
|
||||||
|
[popularityUpdate, ...popularity.events],
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
log('Error returned from popularity.updateAttribute()');
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
log('poplarity updated');
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// return function referralHandler(query, shortId, byAuthenticatedUser) {
|
function incrementArticleViews(article) {
|
||||||
// if (!query.refsource) {
|
return new Promise((resolve, reject) =>
|
||||||
// return null;
|
article.updateAttributes({ $inc: { viewCount: 1 } }, err => {
|
||||||
// }
|
if (err) {
|
||||||
// const eventUpdate = {
|
log(err);
|
||||||
// event: `referral - ${query.refsource}`,
|
return reject(err);
|
||||||
// timestamp: new Date(Date.now()),
|
}
|
||||||
// byAuthenticatedUser
|
log('article views updated');
|
||||||
// };
|
return resolve();
|
||||||
// return Popularity.findOne(
|
})
|
||||||
// { where: { articleId: shortId } },
|
);
|
||||||
// (err, popularity) => {
|
}
|
||||||
// if (err) {
|
|
||||||
// console.error(
|
|
||||||
// 'Failed finding a `Popularity` in a referral handler',
|
|
||||||
// err
|
|
||||||
// );
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (popularity) {
|
return async function handlePopularityStats(req, res, next) {
|
||||||
// return popularity.updateAttribute(
|
const { body, user } = req;
|
||||||
// 'events',
|
|
||||||
// [eventUpdate, ...popularity.events],
|
if (
|
||||||
// err => {
|
!has(body, 'event') ||
|
||||||
// if (err) {
|
!has(body, 'timestamp') ||
|
||||||
// console.error(
|
!has(body, 'shortId')
|
||||||
// 'Failed in updating the `events` attribute of a `popularity`',
|
) {
|
||||||
// err
|
console.warn('Popularity event recieved from client is malformed');
|
||||||
// );
|
console.log(JSON.stringify(body, null, 2));
|
||||||
// }
|
// sending 200 because the client shouldn't care for this
|
||||||
// }
|
return res.sendStatus(200);
|
||||||
// );
|
}
|
||||||
// }
|
const { shortId } = body;
|
||||||
// return Popularity.create(
|
log('shortId', shortId);
|
||||||
// {
|
|
||||||
// events: [eventUpdate],
|
const articlePromise = findArticleByShortId(shortId);
|
||||||
// articleId: shortId
|
const popularityPromise = findPopularityByShortId(shortId);
|
||||||
// },
|
|
||||||
// err => {
|
const [article, popularity] = await Promise.all([
|
||||||
// if (err) {
|
articlePromise,
|
||||||
// return console.error('Failed creating a new `Popularity`', err);
|
popularityPromise
|
||||||
// }
|
]).catch(err => {
|
||||||
// return apiLog('poulartiy created');
|
log('find catch');
|
||||||
// }
|
return next(err);
|
||||||
// );
|
});
|
||||||
// }
|
if (!article || isEmpty(article)) {
|
||||||
// );
|
log('No article found to handle the populartity update');
|
||||||
// };
|
// sending 200 because the client shouldn't care for this
|
||||||
// }
|
return res.sendStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
const populartiyUpdate = {
|
||||||
|
...body,
|
||||||
|
byAuthenticatedUser: !!user
|
||||||
|
};
|
||||||
|
|
||||||
|
const populartiyUpdateOrCreatePromise = isEmpty(popularity)
|
||||||
|
? createPopularity(populartiyUpdate, shortId)
|
||||||
|
: updatePopularity(popularity, populartiyUpdate);
|
||||||
|
const maybeUpdateArticlePromise =
|
||||||
|
body.event === 'view' ? incrementArticleViews(article) : null;
|
||||||
|
return Promise.all([
|
||||||
|
populartiyUpdateOrCreatePromise,
|
||||||
|
maybeUpdateArticlePromise
|
||||||
|
])
|
||||||
|
.then(() => res.sendStatus(200))
|
||||||
|
.catch(err => {
|
||||||
|
log('updates catch');
|
||||||
|
return next(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReferralHandler(app) {
|
||||||
|
const { Popularity } = app.models;
|
||||||
|
|
||||||
|
return function referralHandler(query, shortId, byAuthenticatedUser) {
|
||||||
|
if (!query.refsource) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const eventUpdate = {
|
||||||
|
event: `referral - ${query.refsource}`,
|
||||||
|
timestamp: new Date(Date.now()),
|
||||||
|
byAuthenticatedUser
|
||||||
|
};
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
Popularity.findOne(
|
||||||
|
{ where: { articleId: shortId } },
|
||||||
|
(err, popularity) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(
|
||||||
|
'Failed finding a `Popularity` in a referral handler',
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popularity) {
|
||||||
|
return popularity.updateAttribute(
|
||||||
|
'events',
|
||||||
|
[eventUpdate, ...popularity.events],
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
log('populartiy updated');
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Popularity.create(
|
||||||
|
{
|
||||||
|
events: [eventUpdate],
|
||||||
|
articleId: shortId
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
log('poulartiy created');
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -5,7 +5,8 @@ module.exports = {
|
|||||||
connector: 'mongodb',
|
connector: 'mongodb',
|
||||||
connectionTimeout: 10000,
|
connectionTimeout: 10000,
|
||||||
url: secrets.db,
|
url: secrets.db,
|
||||||
useNewUrlParser: true
|
useNewUrlParser: true,
|
||||||
|
allowExtendedOperators: true
|
||||||
},
|
},
|
||||||
mail: {
|
mail: {
|
||||||
connector: 'mail',
|
connector: 'mail',
|
||||||
|
@ -8,47 +8,10 @@ import { unwrapHandledError } from '../utils/create-handled-error.js';
|
|||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
// const toString = Object.prototype.toString;
|
|
||||||
// is full error or just trace
|
|
||||||
// _.toString(new Error('foo')) => "Error: foo
|
|
||||||
// Object.prototype.toString.call(new Error('foo')) => "[object Error]"
|
|
||||||
// const isInspect = val => !val.stack && _.toString(val) === toString.call(val);
|
|
||||||
// const stringifyErr = val => {
|
|
||||||
// if (val.stack) {
|
|
||||||
// return String(val.stack);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const str = String(val);
|
|
||||||
|
|
||||||
// return isInspect(val) ?
|
|
||||||
// inspect(val) :
|
|
||||||
// str;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const createStackHtml = _.flow(
|
|
||||||
// _.cond([
|
|
||||||
// [isInspect, err => [err]],
|
|
||||||
// // may be stack or just err.msg
|
|
||||||
// [_.stubTrue, _.flow(stringifyErr, _.split('\n'), _.tail) ]
|
|
||||||
// ]),
|
|
||||||
// _.map(_.escape),
|
|
||||||
// _.map(line => `<li>${line}</lin>`),
|
|
||||||
// _.join('')
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const createErrorTitle = _.cond([
|
|
||||||
// [
|
|
||||||
// _.negate(isInspect),
|
|
||||||
// _.flow(stringifyErr, _.split('\n'), _.head, _.defaultTo('Error'))
|
|
||||||
// ],
|
|
||||||
// [_.stubTrue, _.constant('Error')]
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
export default function prodErrorHandler() {
|
export default function prodErrorHandler() {
|
||||||
// error handling in production.
|
// error handling in production.
|
||||||
// disabling eslint due to express parity rules for error handlers
|
// eslint-disable-next-line no-unused-vars
|
||||||
return function(err, req, res, next) {
|
return function(err, req, res, next) {
|
||||||
// eslint-disable-line
|
|
||||||
const handled = unwrapHandledError(err);
|
const handled = unwrapHandledError(err);
|
||||||
// respect handled error status
|
// respect handled error status
|
||||||
let status = handled.status || err.status || res.statusCode;
|
let status = handled.status || err.status || res.statusCode;
|
||||||
|
@ -5,20 +5,30 @@ import {
|
|||||||
unwrapHandledError
|
unwrapHandledError
|
||||||
} from '../utils/create-handled-error.js';
|
} from '../utils/create-handled-error.js';
|
||||||
|
|
||||||
const { ROLLBAR_APP_ID } = process.env;
|
import { rollbar } from '../../../config/secrets';
|
||||||
|
|
||||||
const rollbar = new Rollbar(ROLLBAR_APP_ID);
|
const { appId } = rollbar;
|
||||||
|
const reporter = new Rollbar(appId);
|
||||||
const log = debug('fcc:middlewares:error-reporter');
|
const log = debug('fcc:middlewares:error-reporter');
|
||||||
|
|
||||||
const errTemplate = ({message, ...restError}, req) => `
|
const errTemplate = (error, req) => {
|
||||||
|
const { message, stack } = error;
|
||||||
|
return `
|
||||||
Time: ${new Date(Date.now()).toISOString()}
|
Time: ${new Date(Date.now()).toISOString()}
|
||||||
Error: ${message}
|
Error: ${message}
|
||||||
Is authenticated user: ${!!req.user}
|
Is authenticated user: ${!!req.user}
|
||||||
Route: ${JSON.stringify(req.route, null, 2)}
|
Route: ${JSON.stringify(req.route, null, 2)}
|
||||||
|
Stack: ${stack}
|
||||||
|
|
||||||
${JSON.stringify(restError, null, 2)}
|
// raw
|
||||||
|
${JSON.stringify(error, null, 2)}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function reportError(err) {
|
||||||
|
return reporter.error(err.message, err);
|
||||||
|
}
|
||||||
|
|
||||||
export default function errrorReporter() {
|
export default function errrorReporter() {
|
||||||
if (process.env.NODE_ENV !== 'production' && process.env.ERROR_REPORTER) {
|
if (process.env.NODE_ENV !== 'production' && process.env.ERROR_REPORTER) {
|
||||||
@ -44,6 +54,7 @@ export default function errrorReporter() {
|
|||||||
// logging the error provides us with more information,
|
// logging the error provides us with more information,
|
||||||
// i.e isAuthenticatedUser, req.route
|
// i.e isAuthenticatedUser, req.route
|
||||||
console.error(errTemplate(err, req));
|
console.error(errTemplate(err, req));
|
||||||
return rollbar.error(err.message, err);
|
reportError(err);
|
||||||
|
return next(err);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,17 @@ import { homeLocation } from '../../../config/env';
|
|||||||
|
|
||||||
import { wrapHandledError } from '../utils/create-handled-error';
|
import { wrapHandledError } from '../utils/create-handled-error';
|
||||||
|
|
||||||
|
// We need to tunnel through a proxy path set up within
|
||||||
|
// the gatsby app, at this time, that path is /internal
|
||||||
|
const whiteListRE = new RegExp([
|
||||||
|
'^/internal/n/',
|
||||||
|
'^/internal/p\??'
|
||||||
|
].join('|'));
|
||||||
|
|
||||||
|
|
||||||
export default () => function authorizeByJWT(req, res, next) {
|
export default () => function authorizeByJWT(req, res, next) {
|
||||||
const path = req.path.split('/')[1];
|
const path = req.path.split('/')[1];
|
||||||
if (/^external$|^internal$/.test(path)) {
|
if (/^external$|^internal$/.test(path) && !whiteListRE.test(req.path)) {
|
||||||
const cookie = req.signedCookies && req.signedCookies['jwt_access_token'] ||
|
const cookie = req.signedCookies && req.signedCookies['jwt_access_token'] ||
|
||||||
req.cookie && req.cookie['jwt_access_token'];
|
req.cookie && req.cookie['jwt_access_token'];
|
||||||
if (!cookie) {
|
if (!cookie) {
|
||||||
|
@ -32,7 +32,8 @@ module.exports = {
|
|||||||
'/certification/*',
|
'/certification/*',
|
||||||
'/unsubscribed/*',
|
'/unsubscribed/*',
|
||||||
'/user/*',
|
'/user/*',
|
||||||
'/settings/*'
|
'/settings/*',
|
||||||
|
'/n/*'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -45,6 +46,12 @@ module.exports = {
|
|||||||
curriculumPath: localeChallengesRootDir
|
curriculumPath: localeChallengesRootDir
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
resolve: 'fcc-source-news',
|
||||||
|
options: {
|
||||||
|
maximumStaticRenderCount: 100
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
resolve: 'gatsby-source-filesystem',
|
resolve: 'gatsby-source-filesystem',
|
||||||
options: {
|
options: {
|
||||||
|
@ -8,8 +8,10 @@ const {
|
|||||||
createChallengePages,
|
createChallengePages,
|
||||||
createBlockIntroPages,
|
createBlockIntroPages,
|
||||||
createSuperBlockIntroPages,
|
createSuperBlockIntroPages,
|
||||||
createGuideArticlePages
|
createGuideArticlePages,
|
||||||
|
createNewsArticle
|
||||||
} = require('./utils/gatsby');
|
} = require('./utils/gatsby');
|
||||||
|
const { createArticleSlug } = require('./utils/news');
|
||||||
|
|
||||||
const createByIdentityMap = {
|
const createByIdentityMap = {
|
||||||
guideMarkdown: createGuideArticlePages,
|
guideMarkdown: createGuideArticlePages,
|
||||||
@ -35,6 +37,15 @@ exports.onCreateNode = function onCreateNode({ node, actions, getNode }) {
|
|||||||
createNodeField({ node, name: 'slug', value: slug });
|
createNodeField({ node, name: 'slug', value: slug });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (node.internal.type === 'NewsArticleNode') {
|
||||||
|
const {
|
||||||
|
author: { username },
|
||||||
|
slugPart,
|
||||||
|
shortId
|
||||||
|
} = node;
|
||||||
|
const slug = createArticleSlug({ username, shortId, slugPart });
|
||||||
|
createNodeField({ node, name: 'slug', value: slug });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.createPages = function createPages({ graphql, actions }) {
|
exports.createPages = function createPages({ graphql, actions }) {
|
||||||
@ -86,6 +97,19 @@ exports.createPages = function createPages({ graphql, actions }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
allNewsArticleNode(
|
||||||
|
sort: { fields: firstPublishedDate, order: DESC }
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
shortId
|
||||||
|
fields {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`).then(result => {
|
`).then(result => {
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
@ -126,6 +150,11 @@ exports.createPages = function createPages({ graphql, actions }) {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create news article pages
|
||||||
|
result.data.allNewsArticleNode.edges.forEach(
|
||||||
|
createNewsArticle(createPage)
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -161,7 +190,9 @@ exports.onCreateWebpackConfig = ({ stage, rules, plugins, actions }) => {
|
|||||||
HOME_PATH: JSON.stringify(
|
HOME_PATH: JSON.stringify(
|
||||||
process.env.HOME_PATH || 'http://localhost:3000'
|
process.env.HOME_PATH || 'http://localhost:3000'
|
||||||
),
|
),
|
||||||
STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || '')
|
STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || ''),
|
||||||
|
ROLLBAR_CLIENT_ID: JSON.stringify(process.env.ROLLBAR_CLIENT_ID || ''),
|
||||||
|
ENVIRONMENT: JSON.stringify(process.env.NODE_ENV || 'development')
|
||||||
}),
|
}),
|
||||||
new RmServiceWorkerPlugin()
|
new RmServiceWorkerPlugin()
|
||||||
]
|
]
|
||||||
|
File diff suppressed because one or more lines are too long
101
client/package-lock.json
generated
101
client/package-lock.json
generated
@ -1205,9 +1205,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sinonjs/commons": {
|
"@sinonjs/commons": {
|
||||||
"version": "1.0.2",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz",
|
||||||
"integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==",
|
"integrity": "sha512-j4ZwhaHmwsCb4DlDOIWnI5YyKDNMoNThsmwEpfHx6a1EpsGZ9qYLxP++LMlmBRjtGptGHFsGItJ768snllFWpA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"type-detect": "4.0.8"
|
"type-detect": "4.0.8"
|
||||||
}
|
}
|
||||||
@ -1216,6 +1217,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.0.0.tgz",
|
||||||
"integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==",
|
"integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sinonjs/samsam": "2.1.0"
|
"@sinonjs/samsam": "2.1.0"
|
||||||
},
|
},
|
||||||
@ -1224,6 +1226,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.0.tgz",
|
||||||
"integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==",
|
"integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"array-from": "^2.1.1"
|
"array-from": "^2.1.1"
|
||||||
}
|
}
|
||||||
@ -1231,12 +1234,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sinonjs/samsam": {
|
"@sinonjs/samsam": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.2.tgz",
|
||||||
"integrity": "sha512-7oX6PXMulvdN37h88dvlvRyu61GYZau40fL4wEZvPEHvrjpJc3lDv6xDM5n4Z0StufUVB5nDvVZUM+jZHdMOOQ==",
|
"integrity": "sha512-ZwTHAlC9akprWDinwEPD4kOuwaYZlyMwVJIANsKNC3QVp0AHB04m7RnB4eqeWfgmxw8MGTzS9uMaw93Z3QcZbw==",
|
||||||
"requires": {
|
"dev": true
|
||||||
"array-from": "^2.1.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"@types/configstore": {
|
"@types/configstore": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
@ -1781,7 +1782,8 @@
|
|||||||
"array-from": {
|
"array-from": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
|
||||||
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU="
|
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"array-includes": {
|
"array-includes": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@ -4731,7 +4733,8 @@
|
|||||||
"diff": {
|
"diff": {
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||||
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
|
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"diffie-hellman": {
|
"diffie-hellman": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
@ -10758,7 +10761,8 @@
|
|||||||
"just-extend": {
|
"just-extend": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-3.0.0.tgz",
|
||||||
"integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ=="
|
"integrity": "sha512-Fu3T6pKBuxjWT/p4DkqGHFRsysc8OauWr4ZRTY9dIx07Y9O0RkoR5jcv28aeD1vuAwhm3nLkDurwLXoALp4DpQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"kebab-case": {
|
"kebab-case": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -11017,7 +11021,8 @@
|
|||||||
"lodash.get": {
|
"lodash.get": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash.isequal": {
|
"lodash.isequal": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@ -11143,7 +11148,8 @@
|
|||||||
"lolex": {
|
"lolex": {
|
||||||
"version": "2.7.5",
|
"version": "2.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
|
||||||
"integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q=="
|
"integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"longest-streak": {
|
"longest-streak": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
@ -11779,9 +11785,10 @@
|
|||||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||||
},
|
},
|
||||||
"nise": {
|
"nise": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/nise/-/nise-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/nise/-/nise-1.4.6.tgz",
|
||||||
"integrity": "sha512-OHRVvdxKgwZELf2DTgsJEIA4MOq8XWvpSUzoOXyxJ2mY0mMENWC66+70AShLR2z05B1dzrzWlUQJmJERlOUpZw==",
|
"integrity": "sha512-1GedetLKzmqmgwabuMSqPsT7oumdR77SBpDfNNJhADRIeA3LN/2RVqR4fFqwvzhAqcTef6PPCzQwITE/YQ8S8A==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sinonjs/formatio": "3.0.0",
|
"@sinonjs/formatio": "3.0.0",
|
||||||
"just-extend": "^3.0.0",
|
"just-extend": "^3.0.0",
|
||||||
@ -11793,12 +11800,14 @@
|
|||||||
"isarray": {
|
"isarray": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-to-regexp": {
|
"path-to-regexp": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
|
||||||
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
|
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"isarray": "0.0.1"
|
"isarray": "0.0.1"
|
||||||
}
|
}
|
||||||
@ -14775,6 +14784,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-youtube": {
|
||||||
|
"version": "7.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.8.0.tgz",
|
||||||
|
"integrity": "sha512-kQFR0XTpgGRtzJ54HKDaCtAGr34mgB/BVFeCAL0WUjpIKZBcWtFrKJhYkoKfvWK7aTzJuQ57xojTjG7V6JzILA==",
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^2.0.1",
|
||||||
|
"prop-types": "^15.5.3",
|
||||||
|
"youtube-player": "^5.5.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"read": {
|
"read": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
|
||||||
@ -15923,21 +15949,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sinon": {
|
"sinon": {
|
||||||
"version": "6.3.4",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz",
|
||||||
"integrity": "sha512-NIaR56Z1mefuRBXYrf4otqBxkWiKveX+fvqs3HzFq2b07HcgpkMgIwmQM/owNjNFAHkx0kJXW+Q0mDthiuslXw==",
|
"integrity": "sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@sinonjs/commons": "^1.0.2",
|
"@sinonjs/commons": "^1.0.2",
|
||||||
"@sinonjs/formatio": "^3.0.0",
|
"@sinonjs/formatio": "^3.0.0",
|
||||||
"@sinonjs/samsam": "^2.1.1",
|
"@sinonjs/samsam": "^2.1.2",
|
||||||
"diff": "^3.5.0",
|
"diff": "^3.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lolex": "^2.7.4",
|
"lolex": "^2.7.5",
|
||||||
"nise": "^1.4.5",
|
"nise": "^1.4.5",
|
||||||
"supports-color": "^5.5.0",
|
"supports-color": "^5.5.0",
|
||||||
"type-detect": "^4.0.8"
|
"type-detect": "^4.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sister": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sister/-/sister-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-aG41gNRHRRxPq52MpX4vtm9tapnr6ENmHUx8LMAJWCOplEMwXzh/dp5WIo52Wl8Zlc/VUyHLJ2snX0ck+Nma9g=="
|
||||||
|
},
|
||||||
"sisteransi": {
|
"sisteransi": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz",
|
||||||
@ -17150,7 +17182,8 @@
|
|||||||
"text-encoding": {
|
"text-encoding": {
|
||||||
"version": "0.6.4",
|
"version": "0.6.4",
|
||||||
"resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
|
"resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
|
||||||
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk="
|
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"text-table": {
|
"text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
@ -18953,6 +18986,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||||
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
||||||
},
|
},
|
||||||
|
"youtube-player": {
|
||||||
|
"version": "5.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.1.tgz",
|
||||||
|
"integrity": "sha512-1d62W9She0B1uKNyY6K7jtWFbOW3dYsm6hyKzrh11BLOuYFzkt8K6AcQ3QdPF3aU47dzhZ82clzOJVVWus4HTw==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "^2.6.6",
|
||||||
|
"load-script": "^1.0.0",
|
||||||
|
"sister": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"yurnalist": {
|
"yurnalist": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/yurnalist/-/yurnalist-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/yurnalist/-/yurnalist-0.2.1.tgz",
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
"react-reflex": "^2.2.9",
|
"react-reflex": "^2.2.9",
|
||||||
"react-spinkit": "^3.0.0",
|
"react-spinkit": "^3.0.0",
|
||||||
"react-stripe-elements": "^2.0.1",
|
"react-stripe-elements": "^2.0.1",
|
||||||
|
"react-youtube": "^7.8.0",
|
||||||
"redux": "^4.0.0",
|
"redux": "^4.0.0",
|
||||||
"redux-actions": "^2.6.1",
|
"redux-actions": "^2.6.1",
|
||||||
"redux-devtools-extension": "^2.13.5",
|
"redux-devtools-extension": "^2.13.5",
|
||||||
@ -58,7 +59,6 @@
|
|||||||
"redux-saga": "^0.16.0",
|
"redux-saga": "^0.16.0",
|
||||||
"reselect": "^3.0.1",
|
"reselect": "^3.0.1",
|
||||||
"rxjs": "^6.3.3",
|
"rxjs": "^6.3.3",
|
||||||
"sinon": "^6.3.4",
|
|
||||||
"store": "^2.0.12",
|
"store": "^2.0.12",
|
||||||
"validator": "^10.7.0",
|
"validator": "^10.7.0",
|
||||||
"webpack-remove-serviceworker-plugin": "^1.0.0"
|
"webpack-remove-serviceworker-plugin": "^1.0.0"
|
||||||
@ -88,6 +88,7 @@
|
|||||||
"prettier": "^1.14.2",
|
"prettier": "^1.14.2",
|
||||||
"prettier-eslint-cli": "^4.7.1",
|
"prettier-eslint-cli": "^4.7.1",
|
||||||
"react-test-renderer": "^16.5.2",
|
"react-test-renderer": "^16.5.2",
|
||||||
|
"sinon": "^6.3.5",
|
||||||
"webpack-cli": "^3.1.1"
|
"webpack-cli": "^3.1.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
218
client/plugins/fcc-create-nav-data/package-lock.json
generated
Normal file
218
client/plugins/fcc-create-nav-data/package-lock.json
generated
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
"name": "fcc-create-nav-data",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"assertion-error": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="
|
||||||
|
},
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||||
|
},
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browser-stdout": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
|
||||||
|
},
|
||||||
|
"chai": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
|
||||||
|
"requires": {
|
||||||
|
"assertion-error": "^1.1.0",
|
||||||
|
"check-error": "^1.0.2",
|
||||||
|
"deep-eql": "^3.0.1",
|
||||||
|
"get-func-name": "^2.0.0",
|
||||||
|
"pathval": "^1.1.0",
|
||||||
|
"type-detect": "^4.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"check-error": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII="
|
||||||
|
},
|
||||||
|
"commander": {
|
||||||
|
"version": "2.15.1",
|
||||||
|
"resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
|
||||||
|
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
|
||||||
|
},
|
||||||
|
"concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deep-eql": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
|
||||||
|
"requires": {
|
||||||
|
"type-detect": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diff": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
|
||||||
|
},
|
||||||
|
"escape-string-regexp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||||
|
},
|
||||||
|
"fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||||
|
},
|
||||||
|
"get-func-name": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE="
|
||||||
|
},
|
||||||
|
"glob": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||||
|
"requires": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"growl": {
|
||||||
|
"version": "1.10.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
|
||||||
|
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
|
||||||
|
},
|
||||||
|
"has-flag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||||
|
},
|
||||||
|
"he": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
|
||||||
|
},
|
||||||
|
"inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||||
|
"requires": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
|
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||||
|
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||||
|
},
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||||
|
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||||
|
"requires": {
|
||||||
|
"minimist": "0.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mocha": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
|
||||||
|
"requires": {
|
||||||
|
"browser-stdout": "1.3.1",
|
||||||
|
"commander": "2.15.1",
|
||||||
|
"debug": "3.1.0",
|
||||||
|
"diff": "3.5.0",
|
||||||
|
"escape-string-regexp": "1.0.5",
|
||||||
|
"glob": "7.1.2",
|
||||||
|
"growl": "1.10.5",
|
||||||
|
"he": "1.1.1",
|
||||||
|
"minimatch": "3.0.4",
|
||||||
|
"mkdirp": "0.5.1",
|
||||||
|
"supports-color": "5.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
|
},
|
||||||
|
"once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
|
"requires": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||||
|
},
|
||||||
|
"pathval": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA="
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type-detect": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
|
||||||
|
},
|
||||||
|
"wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,170 +0,0 @@
|
|||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
assertion-error@^1.0.1:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
|
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
|
||||||
version "1.1.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
|
||||||
dependencies:
|
|
||||||
balanced-match "^1.0.0"
|
|
||||||
concat-map "0.0.1"
|
|
||||||
|
|
||||||
browser-stdout@1.3.1:
|
|
||||||
version "1.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
|
|
||||||
|
|
||||||
chai@^4.1.2:
|
|
||||||
version "4.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c"
|
|
||||||
dependencies:
|
|
||||||
assertion-error "^1.0.1"
|
|
||||||
check-error "^1.0.1"
|
|
||||||
deep-eql "^3.0.0"
|
|
||||||
get-func-name "^2.0.0"
|
|
||||||
pathval "^1.0.0"
|
|
||||||
type-detect "^4.0.0"
|
|
||||||
|
|
||||||
check-error@^1.0.1:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
|
||||||
|
|
||||||
commander@2.11.0:
|
|
||||||
version "2.11.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
|
|
||||||
|
|
||||||
concat-map@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
|
||||||
|
|
||||||
debug@3.1.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
|
||||||
dependencies:
|
|
||||||
ms "2.0.0"
|
|
||||||
|
|
||||||
deep-eql@^3.0.0:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
|
|
||||||
dependencies:
|
|
||||||
type-detect "^4.0.0"
|
|
||||||
|
|
||||||
diff@3.5.0:
|
|
||||||
version "3.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
|
||||||
|
|
||||||
escape-string-regexp@1.0.5:
|
|
||||||
version "1.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
|
||||||
|
|
||||||
get-func-name@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
|
|
||||||
|
|
||||||
glob@7.1.2:
|
|
||||||
version "7.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
|
||||||
dependencies:
|
|
||||||
fs.realpath "^1.0.0"
|
|
||||||
inflight "^1.0.4"
|
|
||||||
inherits "2"
|
|
||||||
minimatch "^3.0.4"
|
|
||||||
once "^1.3.0"
|
|
||||||
path-is-absolute "^1.0.0"
|
|
||||||
|
|
||||||
growl@1.10.3:
|
|
||||||
version "1.10.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
|
|
||||||
|
|
||||||
has-flag@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
|
|
||||||
|
|
||||||
he@1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
|
||||||
|
|
||||||
inflight@^1.0.4:
|
|
||||||
version "1.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
|
||||||
dependencies:
|
|
||||||
once "^1.3.0"
|
|
||||||
wrappy "1"
|
|
||||||
|
|
||||||
inherits@2:
|
|
||||||
version "2.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
|
||||||
|
|
||||||
minimatch@^3.0.4:
|
|
||||||
version "3.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
|
||||||
dependencies:
|
|
||||||
brace-expansion "^1.1.7"
|
|
||||||
|
|
||||||
minimist@0.0.8:
|
|
||||||
version "0.0.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
|
||||||
|
|
||||||
mkdirp@0.5.1:
|
|
||||||
version "0.5.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
|
||||||
dependencies:
|
|
||||||
minimist "0.0.8"
|
|
||||||
|
|
||||||
mocha@^5.0.5:
|
|
||||||
version "5.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.5.tgz#e228e3386b9387a4710007a641f127b00be44b52"
|
|
||||||
dependencies:
|
|
||||||
browser-stdout "1.3.1"
|
|
||||||
commander "2.11.0"
|
|
||||||
debug "3.1.0"
|
|
||||||
diff "3.5.0"
|
|
||||||
escape-string-regexp "1.0.5"
|
|
||||||
glob "7.1.2"
|
|
||||||
growl "1.10.3"
|
|
||||||
he "1.1.1"
|
|
||||||
mkdirp "0.5.1"
|
|
||||||
supports-color "4.4.0"
|
|
||||||
|
|
||||||
ms@2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
|
||||||
|
|
||||||
once@^1.3.0:
|
|
||||||
version "1.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
|
||||||
dependencies:
|
|
||||||
wrappy "1"
|
|
||||||
|
|
||||||
path-is-absolute@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
|
||||||
|
|
||||||
pathval@^1.0.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
|
|
||||||
|
|
||||||
supports-color@4.4.0:
|
|
||||||
version "4.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
|
|
||||||
dependencies:
|
|
||||||
has-flag "^2.0.0"
|
|
||||||
|
|
||||||
type-detect@^4.0.0:
|
|
||||||
version "4.0.8"
|
|
||||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
|
||||||
|
|
||||||
wrappy@1:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
|
26
client/plugins/fcc-source-news/create-news-node.js
Normal file
26
client/plugins/fcc-source-news/create-news-node.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function createNewsNode(article) {
|
||||||
|
const contentDigest = crypto
|
||||||
|
.createHash('md5')
|
||||||
|
.update(JSON.stringify(article))
|
||||||
|
.digest('hex');
|
||||||
|
|
||||||
|
const internal = {
|
||||||
|
contentDigest,
|
||||||
|
type: 'NewsArticleNode'
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.parse(
|
||||||
|
JSON.stringify({
|
||||||
|
...article,
|
||||||
|
id: article._id + ' >>>>>>> ' + internal.type,
|
||||||
|
children: [],
|
||||||
|
parent: null,
|
||||||
|
internal,
|
||||||
|
sourceInstanceName: 'article'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createNewsNode = createNewsNode;
|
55
client/plugins/fcc-source-news/gatsby-node.js
Normal file
55
client/plugins/fcc-source-news/gatsby-node.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const path = require('path');
|
||||||
|
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
|
||||||
|
const { MongoClient } = require('mongodb');
|
||||||
|
|
||||||
|
const { createNewsNode } = require('./create-news-node');
|
||||||
|
const { db } = require('../../../config/secrets');
|
||||||
|
|
||||||
|
exports.sourceNodes = function sourceChallengesSourceNodes(
|
||||||
|
{ actions, reporter },
|
||||||
|
pluginOptions
|
||||||
|
) {
|
||||||
|
function handleError(err, client, reject) {
|
||||||
|
if (err) {
|
||||||
|
if (client) {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
reporter.panic(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { maximumStaticRenderCount = 100 } = pluginOptions;
|
||||||
|
const { createNode } = actions;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
MongoClient.connect(
|
||||||
|
db,
|
||||||
|
{ useNewUrlParser: true },
|
||||||
|
async function(err, client) {
|
||||||
|
handleError(err, client, reject);
|
||||||
|
|
||||||
|
reporter.info('fcc-source-news connected successfully to mongo');
|
||||||
|
const db = client.db('freecodecamp');
|
||||||
|
const articleCollection = db.collection('article');
|
||||||
|
|
||||||
|
articleCollection
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { featured: true } },
|
||||||
|
{ $sort: { firstPublishedDate: -1 } },
|
||||||
|
{ $limit: maximumStaticRenderCount }
|
||||||
|
])
|
||||||
|
.toArray((err, articles) => {
|
||||||
|
handleError(err, client, reject);
|
||||||
|
|
||||||
|
articles
|
||||||
|
.map(article => createNewsNode(article))
|
||||||
|
.map(node => createNode(node));
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
reporter.info('fcc-source-news mongo connection closed');
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
80
client/plugins/fcc-source-news/package-lock.json
generated
Normal file
80
client/plugins/fcc-source-news/package-lock.json
generated
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "fcc-source-news",
|
||||||
|
"requires": true,
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"bson": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA=="
|
||||||
|
},
|
||||||
|
"memory-pager": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Mf9OHV/Y7h6YWDxTzX/b4ZZ4oh9NSXblQL8dtPCOomOtZciEHxePR78+uHFLLlsk01A6jVHhHsQZZ/WcIPpnzg==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mongodb": {
|
||||||
|
"version": "3.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.9.tgz",
|
||||||
|
"integrity": "sha512-f+Og32wK/ovzVlC1S6Ft7yjVTvNsAOs6pBpDrPd2/3wPO9ijNsQrTNntuECjOSxGZpPVl0aRqgHzF1e9e+KvnQ==",
|
||||||
|
"requires": {
|
||||||
|
"mongodb-core": "3.1.8",
|
||||||
|
"safe-buffer": "^5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mongodb-core": {
|
||||||
|
"version": "3.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.8.tgz",
|
||||||
|
"integrity": "sha512-reWCqIRNehyuLaqaz5JMOmh3Xd8JIjNX34o8mnewXLK2Fyt/Ky6BZbU+X0OPzy8qbX+JZrOtnuay7ASCieTYZw==",
|
||||||
|
"requires": {
|
||||||
|
"bson": "^1.1.0",
|
||||||
|
"require_optional": "^1.0.1",
|
||||||
|
"safe-buffer": "^5.1.2",
|
||||||
|
"saslprep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require_optional": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
|
||||||
|
"requires": {
|
||||||
|
"resolve-from": "^2.0.0",
|
||||||
|
"semver": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolve-from": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
|
"saslprep": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"sparse-bitfield": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
|
||||||
|
},
|
||||||
|
"sparse-bitfield": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||||
|
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"memory-pager": "^1.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
client/plugins/fcc-source-news/package.json
Normal file
6
client/plugins/fcc-source-news/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "fcc-source-news",
|
||||||
|
"dependencies": {
|
||||||
|
"mongodb": "^3.1.9"
|
||||||
|
}
|
||||||
|
}
|
16
client/src/__mocks__/news-article.js
Normal file
16
client/src/__mocks__/news-article.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export const slugWithId = '/news/quincy/an-article-title--abcDEF123';
|
||||||
|
export const slugWithIdAndQuery =
|
||||||
|
'/news/quincy/an-article-title--abcDEF123?some=query';
|
||||||
|
export const slugWithIdAndHash =
|
||||||
|
'/news/quincy/an-article-title--abcDEF123#top-of-page';
|
||||||
|
export const slugWithIdAndTrailingSlash =
|
||||||
|
'/news/quincy/an-article-title--abcDEF123/';
|
||||||
|
|
||||||
|
export const mockId = 'abcDEF123';
|
||||||
|
export const slugWithoutId = '/news/quincy/an-article-title';
|
||||||
|
|
||||||
|
export const mockArguments = {
|
||||||
|
username: 'quincy',
|
||||||
|
slugPart: 'an-article-title',
|
||||||
|
shortId: 'abcDEF123'
|
||||||
|
};
|
42
client/src/__tests__/integration/handled-error.test.js
Normal file
42
client/src/__tests__/integration/handled-error.test.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/* global describe it expect */
|
||||||
|
import {
|
||||||
|
wrapHandledError,
|
||||||
|
unwrapHandledError
|
||||||
|
} from '../../utils/handled-error';
|
||||||
|
|
||||||
|
describe('handled-error integration', () => {
|
||||||
|
const handledA = {
|
||||||
|
type: 'info',
|
||||||
|
message: 'something helpful',
|
||||||
|
redirectTo: '/a-path-we-choose'
|
||||||
|
};
|
||||||
|
const handledB = {
|
||||||
|
type: 'danger',
|
||||||
|
message: 'Oh noes!',
|
||||||
|
redirectTo: '/whoops'
|
||||||
|
};
|
||||||
|
const handledC = {
|
||||||
|
type: 'success',
|
||||||
|
message: 'great news!',
|
||||||
|
redirectTo: '/awesome'
|
||||||
|
};
|
||||||
|
const handledD = {};
|
||||||
|
|
||||||
|
it('can wrap and unwrap handled errors', () => {
|
||||||
|
expect.assertions(4);
|
||||||
|
const wrappedA = wrapHandledError(new Error(), handledA);
|
||||||
|
const wrappedB = wrapHandledError(new Error(), handledB);
|
||||||
|
const wrappedC = wrapHandledError(new Error(), handledC);
|
||||||
|
const wrappedD = wrapHandledError(new Error(), handledD);
|
||||||
|
|
||||||
|
const unwrappedA = unwrapHandledError(wrappedA);
|
||||||
|
const unwrappedB = unwrapHandledError(wrappedB);
|
||||||
|
const unwrappedC = unwrapHandledError(wrappedC);
|
||||||
|
const unwrappedD = unwrapHandledError(wrappedD);
|
||||||
|
|
||||||
|
expect(unwrappedA).toEqual(handledA);
|
||||||
|
expect(unwrappedB).toEqual(handledB);
|
||||||
|
expect(unwrappedC).toEqual(handledC);
|
||||||
|
expect(unwrappedD).toEqual(handledD);
|
||||||
|
});
|
||||||
|
});
|
33
client/src/__tests__/integration/news-slug.test.js
Normal file
33
client/src/__tests__/integration/news-slug.test.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* global describe it expect */
|
||||||
|
import faker from 'faker';
|
||||||
|
import { kebabCase } from 'lodash';
|
||||||
|
import shortid from 'shortid';
|
||||||
|
|
||||||
|
import { createArticleSlug } from '../../../utils/news';
|
||||||
|
import { getShortIdFromSlug } from '../../utils';
|
||||||
|
|
||||||
|
shortid.characters(
|
||||||
|
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$+'
|
||||||
|
);
|
||||||
|
const shortId = () => shortid.generate();
|
||||||
|
|
||||||
|
describe('news-slug integration', () => {
|
||||||
|
it('returns the correct id from a generated slug', () => {
|
||||||
|
expect.assertions(100);
|
||||||
|
|
||||||
|
const generatedArguments = Array(100)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => ({
|
||||||
|
username: faker.internet.userName(),
|
||||||
|
slugPart: kebabCase(faker.lorem.sentence()).toLowerCase(),
|
||||||
|
shortId: shortId()
|
||||||
|
}));
|
||||||
|
|
||||||
|
return generatedArguments.forEach(arg => {
|
||||||
|
const { shortId } = arg;
|
||||||
|
const generatedSlug = createArticleSlug(arg);
|
||||||
|
const extractedId = getShortIdFromSlug(generatedSlug);
|
||||||
|
return expect(extractedId).toEqual(shortId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
89
client/src/client-only-routes/ShowDynamicNewsOrFourOhFour.js
Normal file
89
client/src/client-only-routes/ShowDynamicNewsOrFourOhFour.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { isNull, pick, isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
import Layout from '../components/layouts/Default';
|
||||||
|
import Loader from '../components/helpers/Loader';
|
||||||
|
|
||||||
|
import { getShortIdFromSlug } from '../utils';
|
||||||
|
import { createArticleSlug } from '../../utils/news';
|
||||||
|
import {
|
||||||
|
resolveShortId,
|
||||||
|
resolveShortIdFetchStateSelector,
|
||||||
|
dynamicArticleByIdMapSelector
|
||||||
|
} from '../templates/News/redux';
|
||||||
|
import { createFlashMessage } from '../components/Flash/redux';
|
||||||
|
import ShowArticle from '../templates/News/ShowArticle';
|
||||||
|
|
||||||
|
const mapStateToProps = () => (state, { articleSlug = '' }) => {
|
||||||
|
const shortId = getShortIdFromSlug(articleSlug);
|
||||||
|
const articleMap = dynamicArticleByIdMapSelector(state);
|
||||||
|
const article = articleMap[shortId] || null;
|
||||||
|
const fetchState = resolveShortIdFetchStateSelector(state);
|
||||||
|
return { article, fetchState, shortId };
|
||||||
|
};
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators({ createFlashMessage, resolveShortId }, dispatch);
|
||||||
|
|
||||||
|
class DynamicNewsArticle extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { shortId, article, resolveShortId } = this.props;
|
||||||
|
if (isNull(article)) {
|
||||||
|
return resolveShortId(shortId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getArticleAsGatsbyProps(article) {
|
||||||
|
const {
|
||||||
|
author: { username },
|
||||||
|
slugPart,
|
||||||
|
shortId,
|
||||||
|
meta: { readTime }
|
||||||
|
} = article;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
newsArticleNode: {
|
||||||
|
...pick(article, [
|
||||||
|
'title',
|
||||||
|
'renderableContent',
|
||||||
|
'youtube',
|
||||||
|
'author',
|
||||||
|
'firstPublishedDate',
|
||||||
|
'shortId',
|
||||||
|
'featureImage'
|
||||||
|
]),
|
||||||
|
fields: { slug: createArticleSlug({ username, slugPart, shortId }) },
|
||||||
|
meta: { readTime }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fetchState: { pending },
|
||||||
|
article
|
||||||
|
} = this.props;
|
||||||
|
if (pending) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className='loader-wrapper'>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return isEmpty(article) ? null : (
|
||||||
|
<ShowArticle {...this.getArticleAsGatsbyProps(article)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicNewsArticle.displayName = 'DynamicNewsArticle';
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DynamicNewsArticle);
|
@ -23,8 +23,7 @@ const propTypes = {
|
|||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
profileUI: PropTypes.object
|
profileUI: PropTypes.object
|
||||||
}),
|
}),
|
||||||
showLoading: PropTypes.bool,
|
showLoading: PropTypes.bool
|
||||||
splat: PropTypes.string
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRequestedUserSelector = () => (state, { maybeUser }) =>
|
const createRequestedUserSelector = () => (state, { maybeUser }) =>
|
||||||
@ -49,22 +48,15 @@ const mapDispatchToProps = dispatch =>
|
|||||||
|
|
||||||
class ShowFourOhFour extends Component {
|
class ShowFourOhFour extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { requestedUser, maybeUser, splat, fetchProfileForUser } = this.props;
|
const { requestedUser, maybeUser, fetchProfileForUser } = this.props;
|
||||||
if (!splat && isEmpty(requestedUser)) {
|
if (isEmpty(requestedUser)) {
|
||||||
console.log(requestedUser);
|
|
||||||
return fetchProfileForUser(maybeUser);
|
return fetchProfileForUser(maybeUser);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isSessionUser, requestedUser, showLoading, splat } = this.props;
|
const { isSessionUser, requestedUser, showLoading } = this.props;
|
||||||
if (splat) {
|
|
||||||
// the uri path for this component is /:maybeUser/:splat
|
|
||||||
// if splat is defined then we on a route that is not a profile
|
|
||||||
// and we should just 404
|
|
||||||
return <FourOhFourPage />;
|
|
||||||
}
|
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
// We don't know if /:maybeUser is a user or not, we will show the loader
|
// We don't know if /:maybeUser is a user or not, we will show the loader
|
||||||
// until we get a response from the API
|
// until we get a response from the API
|
||||||
|
@ -3,7 +3,7 @@ import nanoId from 'nanoid';
|
|||||||
|
|
||||||
import { createTypes } from '../../../utils/createTypes';
|
import { createTypes } from '../../../utils/createTypes';
|
||||||
|
|
||||||
const ns = 'flash';
|
export const ns = 'flash';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
messages: []
|
messages: []
|
||||||
|
@ -7,7 +7,6 @@ header {
|
|||||||
|
|
||||||
#top-nav {
|
#top-nav {
|
||||||
background: #006400;
|
background: #006400;
|
||||||
margin-bottom: 0.45rem;
|
|
||||||
height: 38px;
|
height: 38px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -42,7 +41,8 @@ header {
|
|||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
#top-right-nav a, #top-right-nav img {
|
#top-right-nav a,
|
||||||
|
#top-right-nav img {
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,15 +62,43 @@ header {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0 3px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#top-right-nav li > a, #top-right-nav li > span {
|
#top-right-nav li,
|
||||||
color:#fff;
|
#top-right-nav li > a {
|
||||||
|
color: #fff;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#top-right-nav li > a:hover, #top-right-nav li > a:focus {
|
#top-right-nav li:hover,
|
||||||
|
#top-right-nav li:hover a,
|
||||||
|
#top-right-nav li > a:hover,
|
||||||
|
#top-right-nav li:focus,
|
||||||
|
#top-right-nav li:focus a,
|
||||||
|
#top-right-nav li > a:focus {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #006400;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.user-state-link,
|
||||||
|
li.user-state-link:hover,
|
||||||
|
li.user-state-link:focus,
|
||||||
|
li.user-state-link > a,
|
||||||
|
li.user-state-link > a:hover,
|
||||||
|
li.user-state-link > a:focus {
|
||||||
|
background-color: #006400 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-right-nav {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
#top-right-nav li:last-child {
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-right-nav li > a:hover,
|
||||||
|
#top-right-nav li > a:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,9 @@ function Header({ disableSettings }) {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
<Link to='/news'>News</Link>
|
||||||
|
</li>
|
||||||
|
<li className='user-state-link'>
|
||||||
<UserState disableSettings={disableSettings} />
|
<UserState disableSettings={disableSettings} />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -2,7 +2,7 @@ import { createAction, handleActions } from 'redux-actions';
|
|||||||
|
|
||||||
import { createTypes } from '../../../../utils/stateManagement';
|
import { createTypes } from '../../../../utils/stateManagement';
|
||||||
|
|
||||||
const ns = 'curriculumMap';
|
export const ns = 'curriculumMap';
|
||||||
|
|
||||||
export const getNS = () => ns;
|
export const getNS = () => ns;
|
||||||
|
|
||||||
|
@ -1,10 +1,3 @@
|
|||||||
import { navigate } from 'gatsby';
|
import createRedirect from './createRedirect';
|
||||||
|
|
||||||
const RedirectHome = () => {
|
export default createRedirect('/');
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RedirectHome;
|
|
||||||
|
3
client/src/components/RedirectNews.js
Normal file
3
client/src/components/RedirectNews.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import createRedirect from './createRedirect';
|
||||||
|
|
||||||
|
export default createRedirect('/news');
|
10
client/src/components/createRedirect.js
Normal file
10
client/src/components/createRedirect.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { navigate } from 'gatsby';
|
||||||
|
|
||||||
|
const createRedirect = (to = '/') => () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
navigate(to);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createRedirect;
|
@ -1,13 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Router } from '@reach/router';
|
import { Router } from '@reach/router';
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
|
import NotFoundPage from '../components/FourOhFour';
|
||||||
|
import RedirectNews from '../components/RedirectNews';
|
||||||
|
/* eslint-disable max-len */
|
||||||
import ShowProfileOrFourOhFour from '../client-only-routes/ShowProfileOrFourOhFour';
|
import ShowProfileOrFourOhFour from '../client-only-routes/ShowProfileOrFourOhFour';
|
||||||
|
import ShowDynamicNewsOrFourOhFour from '../client-only-routes/ShowDynamicNewsOrFourOhFour';
|
||||||
|
/* eslint-enable max-len */
|
||||||
|
|
||||||
function FourOhFourPage() {
|
function FourOhFourPage() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<ShowProfileOrFourOhFour path='/:maybeUser/:splat' />
|
|
||||||
<ShowProfileOrFourOhFour path='/:maybeUser' />
|
<ShowProfileOrFourOhFour path='/:maybeUser' />
|
||||||
|
<ShowDynamicNewsOrFourOhFour path='/news/:author/:articleSlug' />
|
||||||
|
<RedirectNews path='/news/:author' />
|
||||||
|
<NotFoundPage default={true} />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
15
client/src/pages/n.js
Normal file
15
client/src/pages/n.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Router } from '@reach/router';
|
||||||
|
|
||||||
|
import NewsReferalLinkHandler from '../templates/News/NewsReferalLinkHandler';
|
||||||
|
import RedirectNews from '../components/RedirectNews';
|
||||||
|
|
||||||
|
export default function NewsReferalLinkRouter() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<NewsReferalLinkHandler path='/n/:shortId' />
|
||||||
|
<RedirectNews path='/n/:shortId/:splat' />
|
||||||
|
<RedirectNews path='/n' />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
96
client/src/pages/news.js
Normal file
96
client/src/pages/news.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Grid } from '@freecodecamp/react-bootstrap';
|
||||||
|
import { graphql } from 'gatsby';
|
||||||
|
|
||||||
|
import Layout from '../components/layouts/Default';
|
||||||
|
|
||||||
|
import FullWidthRow from '../components/helpers/FullWidthRow';
|
||||||
|
import Featured from '../templates/News/Featured';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
allNewsArticleNode: PropTypes.shape({
|
||||||
|
edges: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
title: PropTypes.string,
|
||||||
|
shortId: PropTypes.string,
|
||||||
|
slugPart: PropTypes.string,
|
||||||
|
featureImage: PropTypes.shape({
|
||||||
|
src: PropTypes.string,
|
||||||
|
alt: PropTypes.string,
|
||||||
|
caption: PropTypes.string
|
||||||
|
}),
|
||||||
|
meta: PropTypes.shape({
|
||||||
|
readTime: PropTypes.number
|
||||||
|
}),
|
||||||
|
author: PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
avatar: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
username: PropTypes.string,
|
||||||
|
bio: PropTypes.string
|
||||||
|
}),
|
||||||
|
viewCount: PropTypes.number,
|
||||||
|
firstPublishedDate: PropTypes.string
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NewsIndexPage(props) {
|
||||||
|
const {
|
||||||
|
allNewsArticleNode: { edges }
|
||||||
|
} = props.data;
|
||||||
|
const articles = edges.map(({ node }) => node);
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Grid>
|
||||||
|
<FullWidthRow>
|
||||||
|
<h1>News - freeCodeCamp.org</h1>
|
||||||
|
</FullWidthRow>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Featured featuredList={articles} />
|
||||||
|
</FullWidthRow>
|
||||||
|
</Grid>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const query = graphql`
|
||||||
|
{
|
||||||
|
allNewsArticleNode(sort: { fields: firstPublishedDate, order: DESC }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
shortId
|
||||||
|
slugPart
|
||||||
|
featureImage {
|
||||||
|
src
|
||||||
|
alt
|
||||||
|
caption
|
||||||
|
}
|
||||||
|
meta {
|
||||||
|
readTime
|
||||||
|
}
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
twitter
|
||||||
|
bio
|
||||||
|
username
|
||||||
|
}
|
||||||
|
viewCount
|
||||||
|
firstPublishedDate
|
||||||
|
fields {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
NewsIndexPage.displayName = 'NewsIndexPage';
|
||||||
|
NewsIndexPage.propTypes = propTypes;
|
25
client/src/redux/error-saga.js
Normal file
25
client/src/redux/error-saga.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { navigate } from 'gatsby';
|
||||||
|
import { takeEvery, put } from 'redux-saga/effects';
|
||||||
|
import { isError } from 'lodash';
|
||||||
|
|
||||||
|
import { isHandledError, unwrapHandledError } from '../utils/handled-error';
|
||||||
|
import { reportClientSideError } from '../utils/report-error';
|
||||||
|
import { createFlashMessage } from '../components/Flash/redux';
|
||||||
|
import reportedErrorMessage from '../utils/reportedErrorMessage';
|
||||||
|
|
||||||
|
const errorActionSelector = action => isError(action.payload);
|
||||||
|
|
||||||
|
function* errorHandlerSaga({ payload: error }) {
|
||||||
|
if (isHandledError(error)) {
|
||||||
|
const { type, message, redirectTo } = unwrapHandledError(error);
|
||||||
|
if (redirectTo) {
|
||||||
|
navigate(redirectTo);
|
||||||
|
}
|
||||||
|
yield put(createFlashMessage({ type, message }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reportClientSideError('Unhandled Error caught in error-saga', error);
|
||||||
|
yield put(createFlashMessage(reportedErrorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default [takeEvery(errorActionSelector, errorHandlerSaga)];
|
@ -19,9 +19,9 @@ import { types as settingsTypes } from './settings';
|
|||||||
const challengeReduxTypes = {};
|
const challengeReduxTypes = {};
|
||||||
/** ***********************************/
|
/** ***********************************/
|
||||||
|
|
||||||
const ns = 'app';
|
export const ns = 'app';
|
||||||
|
|
||||||
const defaultFetchState = {
|
export const defaultFetchState = {
|
||||||
pending: true,
|
pending: true,
|
||||||
complete: false,
|
complete: false,
|
||||||
errored: false,
|
errored: false,
|
||||||
@ -220,16 +220,15 @@ export const reducer = handleActions(
|
|||||||
[username]: { ...previousUserObject, ...user }
|
[username]: { ...previousUserObject, ...user }
|
||||||
},
|
},
|
||||||
userProfileFetchState: {
|
userProfileFetchState: {
|
||||||
|
...defaultFetchState,
|
||||||
pending: false,
|
pending: false,
|
||||||
complete: true,
|
complete: true
|
||||||
errored: false,
|
|
||||||
error: null
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[types.fetchProfileForUserError]: (state, { payload }) => ({
|
[types.fetchProfileForUserError]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
userFetchState: {
|
userProfileFetchState: {
|
||||||
pending: false,
|
pending: false,
|
||||||
complete: false,
|
complete: false,
|
||||||
errored: true,
|
errored: true,
|
||||||
@ -253,10 +252,9 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
showCert: payload,
|
showCert: payload,
|
||||||
showCertFetchState: {
|
showCertFetchState: {
|
||||||
|
...defaultFetchState,
|
||||||
pending: false,
|
pending: false,
|
||||||
complete: true,
|
complete: true
|
||||||
errored: false,
|
|
||||||
error: null
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[types.showCertError]: (state, { payload }) => ({
|
[types.showCertError]: (state, { payload }) => ({
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import {reducer as formReducer} from 'redux-form';
|
import { reducer as formReducer } from 'redux-form';
|
||||||
|
|
||||||
import { reducer as app } from './';
|
import { reducer as app, ns as appNameSpace } from './';
|
||||||
import { reducer as flash } from '../components/Flash/redux';
|
import {
|
||||||
import { reducer as settings } from './settings';
|
reducer as flash,
|
||||||
import { reducer as curriculumMap } from '../components/Map/redux';
|
ns as flashNameSpace
|
||||||
import { reducer as challenge } from '../templates/Challenges/redux';
|
} from '../components/Flash/redux';
|
||||||
|
import { reducer as settings, ns as settingsNameSpace } from './settings';
|
||||||
|
import {
|
||||||
|
reducer as curriculumMap,
|
||||||
|
ns as curriculumMapNameSpace
|
||||||
|
} from '../components/Map/redux';
|
||||||
|
import {
|
||||||
|
reducer as challenge,
|
||||||
|
ns as challengeNameSpace
|
||||||
|
} from '../templates/Challenges/redux';
|
||||||
|
import { reducer as news, ns as newsNameSpace } from '../templates/News/redux';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
app,
|
[appNameSpace]: app,
|
||||||
challenge,
|
[challengeNameSpace]: challenge,
|
||||||
curriculumMap,
|
[curriculumMapNameSpace]: curriculumMap,
|
||||||
flash,
|
[flashNameSpace]: flash,
|
||||||
form: formReducer,
|
form: formReducer,
|
||||||
settings
|
[newsNameSpace]: news,
|
||||||
|
[settingsNameSpace]: settings
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { all } from 'redux-saga/effects';
|
import { all } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import errorSagas from './error-saga';
|
||||||
import { sagas as appSagas } from './';
|
import { sagas as appSagas } from './';
|
||||||
import { sagas as settingsSagas } from './settings';
|
|
||||||
import { sagas as challengeSagas } from '../templates/Challenges/redux';
|
import { sagas as challengeSagas } from '../templates/Challenges/redux';
|
||||||
|
import { sagas as newsSagas } from '../templates/News/redux';
|
||||||
|
import { sagas as settingsSagas } from './settings';
|
||||||
|
|
||||||
export default function* rootSaga() {
|
export default function* rootSaga() {
|
||||||
yield all([...appSagas, ...challengeSagas, ...settingsSagas]);
|
yield all([
|
||||||
|
...errorSagas,
|
||||||
|
...appSagas,
|
||||||
|
...challengeSagas,
|
||||||
|
...newsSagas,
|
||||||
|
...settingsSagas
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { createTypes, createAsyncTypes } from '../../utils/createTypes';
|
|||||||
import { createSettingsSagas } from './settings-sagas';
|
import { createSettingsSagas } from './settings-sagas';
|
||||||
import { createUpdateMyEmailSaga } from './update-email-saga';
|
import { createUpdateMyEmailSaga } from './update-email-saga';
|
||||||
|
|
||||||
const ns = 'settings';
|
export const ns = 'settings';
|
||||||
|
|
||||||
const defaultFetchState = {
|
const defaultFetchState = {
|
||||||
pending: false,
|
pending: false,
|
||||||
|
@ -15,7 +15,7 @@ import currentChallengeEpic from './current-challenge-epic';
|
|||||||
|
|
||||||
import { createIdToNameMapSaga } from './id-to-name-map-saga';
|
import { createIdToNameMapSaga } from './id-to-name-map-saga';
|
||||||
|
|
||||||
const ns = 'challenge';
|
export const ns = 'challenge';
|
||||||
export const backendNS = 'backendChallenge';
|
export const backendNS = 'backendChallenge';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
62
client/src/templates/News/Featured/Featured.js
Normal file
62
client/src/templates/News/Featured/Featured.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { navigate } from 'gatsby';
|
||||||
|
import { Image } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
|
import Spacer from '../../../components/helpers/Spacer';
|
||||||
|
import BannerWide from '../components/BannerWide';
|
||||||
|
import ArticleMeta from '../components/ArticleMeta';
|
||||||
|
|
||||||
|
import './featured.css';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
featuredList: PropTypes.arrayOf(PropTypes.object)
|
||||||
|
};
|
||||||
|
|
||||||
|
class Featured extends Component {
|
||||||
|
createHandleArticleClick(slug) {
|
||||||
|
return e => {
|
||||||
|
e.preventDefault();
|
||||||
|
return navigate(slug);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFeatured(articles) {
|
||||||
|
return articles.map(article => {
|
||||||
|
const { featureImage, shortId, title, fields: {slug} } = article;
|
||||||
|
return (
|
||||||
|
<li className='featured-list-item' key={shortId}>
|
||||||
|
<a
|
||||||
|
href={'/news' + slug}
|
||||||
|
onClick={this.createHandleArticleClick(slug)}
|
||||||
|
>
|
||||||
|
<h3 className='title'>{title}</h3>
|
||||||
|
{featureImage && featureImage.src ? (
|
||||||
|
<Image
|
||||||
|
className='featured-list-image'
|
||||||
|
responsive={true}
|
||||||
|
src={featureImage.src}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BannerWide />
|
||||||
|
)}
|
||||||
|
<ArticleMeta article={article} />
|
||||||
|
</a>
|
||||||
|
<Spacer />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { featuredList } = this.props;
|
||||||
|
return (
|
||||||
|
<ul className='featured-list'>{this.renderFeatured(featuredList)}</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Featured.displayName = 'Featured';
|
||||||
|
Featured.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Featured;
|
33
client/src/templates/News/Featured/featured.css
Normal file
33
client/src/templates/News/Featured/featured.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
.featured-list {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list-item {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list-item .title {
|
||||||
|
color: #333;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list-item a {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list-image {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list-item a:hover,
|
||||||
|
.featured-list-item a:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
text-decoration-line: none;
|
||||||
|
text-decoration-color: transparaent;
|
||||||
|
}
|
||||||
|
.featured-list-item a:hover > .meta-wrapper,
|
||||||
|
.featured-list-item a:focus > .meta-wrapper {
|
||||||
|
color: #006400;
|
||||||
|
}
|
61
client/src/templates/News/NewsReferalLinkHandler/index.js
Normal file
61
client/src/templates/News/NewsReferalLinkHandler/index.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { navigate } from 'gatsby';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import Layout from '../../../components/layouts/Default';
|
||||||
|
import { resolveShortId, dynamicArticleSelector } from '../redux';
|
||||||
|
import { Loader } from '../../../components/helpers';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
redirect: PropTypes.string,
|
||||||
|
resolveShortId: PropTypes.func.isRequired,
|
||||||
|
shortId: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = () => (state, props) => {
|
||||||
|
const article = dynamicArticleSelector(state, props);
|
||||||
|
return {
|
||||||
|
redirect: article.redirect
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators({ resolveShortId }, dispatch);
|
||||||
|
|
||||||
|
class NewsReferalLinkHandler extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { shortId, redirect, resolveShortId } = this.props;
|
||||||
|
if (!redirect) {
|
||||||
|
return resolveShortId(shortId);
|
||||||
|
}
|
||||||
|
return navigate(redirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
const { redirect } = this.props;
|
||||||
|
if (redirect) {
|
||||||
|
return navigate(redirect);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className='loader-wrapper'>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NewsReferalLinkHandler.displayName = 'NewsReferalLinkHandler';
|
||||||
|
NewsReferalLinkHandler.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(NewsReferalLinkHandler);
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
import ArticleMeta from '../../../components/ArticleMeta';
|
import ArticleMeta from '../../components/ArticleMeta';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
article: PropTypes.shape({
|
article: PropTypes.shape({
|
187
client/src/templates/News/ShowArticle/index.js
Normal file
187
client/src/templates/News/ShowArticle/index.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
import { graphql } from 'gatsby';
|
||||||
|
import Youtube from 'react-youtube';
|
||||||
|
import { Image, Grid } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
|
import Layout from '../../../components/layouts/Default';
|
||||||
|
import Author from './components/Author';
|
||||||
|
import FullWidthRow from '../../../components/helpers/FullWidthRow';
|
||||||
|
import Spacer from '../../../components/helpers/Spacer';
|
||||||
|
import { postPopularityEvent } from '../../../utils/ajax';
|
||||||
|
import { newsArticleNodePropTypes } from './proptypes';
|
||||||
|
|
||||||
|
import './show-article.css';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
newsArticleNode: newsArticleNodePropTypes
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const youtubeOpts = {
|
||||||
|
playerVars: {
|
||||||
|
// https://developers.google.com/youtube/player_parameters
|
||||||
|
autoplay: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ShowArticle extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fetchState: {
|
||||||
|
pending: false,
|
||||||
|
complete: false,
|
||||||
|
errored: false,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
currentArticle: {},
|
||||||
|
requiredArticle: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
|
const { shortId } = this.props.data.newsArticleNode;
|
||||||
|
return postPopularityEvent({
|
||||||
|
event: 'view',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
shortId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeReady(event) {
|
||||||
|
event.target.pauseVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
newsArticleNode: {
|
||||||
|
title,
|
||||||
|
renderableContent,
|
||||||
|
youtubeId,
|
||||||
|
featureImage,
|
||||||
|
fields: { slug }
|
||||||
|
},
|
||||||
|
newsArticleNode
|
||||||
|
}
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// RegEx finds the first paragraph and groups the content
|
||||||
|
const description = renderableContent.join('').match(/<p>(.*?)<\/p>/)[1];
|
||||||
|
const pageTitle = `${title} | freeCodeCamp.org`;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Helmet>
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
<link
|
||||||
|
href={`https://www.freecodecamp.org/news${slug}`}
|
||||||
|
rel='canonical'
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
content={`https://www.freecodecamp.org/news${slug}`}
|
||||||
|
property='og:url'
|
||||||
|
/>
|
||||||
|
<meta content={pageTitle} property='og:title' />
|
||||||
|
<meta content={description} property='og:description' />
|
||||||
|
<meta content={description} name='description' />
|
||||||
|
<meta content={featureImage.src} property='og:image' />
|
||||||
|
</Helmet>
|
||||||
|
<Layout>
|
||||||
|
<article className='show-article'>
|
||||||
|
<Spacer />
|
||||||
|
<Author article={newsArticleNode} />
|
||||||
|
<Grid>
|
||||||
|
<FullWidthRow>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
</FullWidthRow>
|
||||||
|
{featureImage ? (
|
||||||
|
<FullWidthRow>
|
||||||
|
<div className='feature-image-wrapper'>
|
||||||
|
<figure>
|
||||||
|
<Image
|
||||||
|
alt={featureImage.alt}
|
||||||
|
responsive={true}
|
||||||
|
src={featureImage.src}
|
||||||
|
/>
|
||||||
|
{featureImage.caption ? (
|
||||||
|
<figcaption
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: featureImage.caption
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</FullWidthRow>
|
||||||
|
) : null}
|
||||||
|
<FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: renderableContent.join('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
{youtubeId ? (
|
||||||
|
<Fragment>
|
||||||
|
<div className='youtube-wrapper'>
|
||||||
|
<Youtube
|
||||||
|
onReady={this.youtubeReady}
|
||||||
|
opts={youtubeOpts}
|
||||||
|
videoId={youtubeId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Spacer />
|
||||||
|
</Fragment>
|
||||||
|
) : null}
|
||||||
|
</Grid>
|
||||||
|
</article>
|
||||||
|
</Layout>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowArticle.displayName = 'ShowArticle';
|
||||||
|
ShowArticle.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ShowArticle;
|
||||||
|
|
||||||
|
export const query = graphql`
|
||||||
|
fragment newsArticleContent on NewsArticleNode {
|
||||||
|
title
|
||||||
|
renderableContent
|
||||||
|
featureImage {
|
||||||
|
src
|
||||||
|
alt
|
||||||
|
caption
|
||||||
|
}
|
||||||
|
fields {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
author {
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
twitter
|
||||||
|
bio
|
||||||
|
username
|
||||||
|
}
|
||||||
|
meta {
|
||||||
|
readTime
|
||||||
|
}
|
||||||
|
firstPublishedDate
|
||||||
|
shortId
|
||||||
|
}
|
||||||
|
|
||||||
|
query NewsArticleById($id: String!) {
|
||||||
|
newsArticleNode(id: { eq: $id }) {
|
||||||
|
...newsArticleContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
26
client/src/templates/News/ShowArticle/proptypes.js
Normal file
26
client/src/templates/News/ShowArticle/proptypes.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export const newsArticleNodePropTypes = PropTypes.shape({
|
||||||
|
title: PropTypes.string,
|
||||||
|
renderableContent: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
featureImage: PropTypes.shape({
|
||||||
|
src: PropTypes.string,
|
||||||
|
alt: PropTypes.string,
|
||||||
|
caption: PropTypes.string
|
||||||
|
}),
|
||||||
|
fields: PropTypes.shape({
|
||||||
|
slug: PropTypes.string
|
||||||
|
}),
|
||||||
|
author: PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
avatar: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
bio: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
}),
|
||||||
|
meta: PropTypes.shape({
|
||||||
|
readTime: PropTypes.number
|
||||||
|
}),
|
||||||
|
firstPublishedDate: PropTypes.string,
|
||||||
|
shortId: PropTypes.string
|
||||||
|
});
|
34
client/src/templates/News/ShowArticle/show-article.css
Normal file
34
client/src/templates/News/ShowArticle/show-article.css
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.show-article figure {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-article figcaption > * {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-article figcaption {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-article a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-image-wrapper {
|
||||||
|
padding-top: 32px;
|
||||||
|
}
|
||||||
|
.youtube-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 56.25%; /* 16:9 */
|
||||||
|
padding-top: 25px;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.youtube-wrapper iframe {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 95%;
|
||||||
|
}
|
67
client/src/templates/News/redux/index.js
Normal file
67
client/src/templates/News/redux/index.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { createAction, handleActions } from 'redux-actions';
|
||||||
|
|
||||||
|
import { createTypes } from '../../../../utils/stateManagement';
|
||||||
|
import { createAsyncTypes } from '../../../utils/createTypes';
|
||||||
|
import { defaultFetchState } from '../../../redux';
|
||||||
|
import { createShortIdSaga } from './shortId-saga';
|
||||||
|
|
||||||
|
export const ns = 'news';
|
||||||
|
const initialState = {
|
||||||
|
resolveShortIdFetchState: { ...defaultFetchState },
|
||||||
|
dynamicArticleByIdMap: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const types = createTypes([...createAsyncTypes('resolveShortId')], ns);
|
||||||
|
|
||||||
|
export const sagas = [...createShortIdSaga(types)];
|
||||||
|
|
||||||
|
export const resolveShortId = createAction(types.resolveShortId);
|
||||||
|
export const resolveShortIdComplete = createAction(
|
||||||
|
types.resolveShortIdComplete,
|
||||||
|
article => {
|
||||||
|
const { slug } = article;
|
||||||
|
article.redirect = slug;
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export const resolveShortIdError = createAction(types.resolveShortIdError);
|
||||||
|
|
||||||
|
export const resolveShortIdFetchStateSelector = state =>
|
||||||
|
state[ns].resolveShortIdFetchState;
|
||||||
|
export const dynamicArticleByIdMapSelector = state =>
|
||||||
|
state[ns].dynamicArticleByIdMap;
|
||||||
|
export const dynamicArticleSelector = (state, { shortId }) => {
|
||||||
|
const map = dynamicArticleByIdMapSelector(state);
|
||||||
|
return map[shortId] || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducer = handleActions(
|
||||||
|
{
|
||||||
|
[types.resolveShortId]: state => ({
|
||||||
|
...state,
|
||||||
|
resolveShortIdFetchState: { ...defaultFetchState }
|
||||||
|
}),
|
||||||
|
[types.resolveShortIdComplete]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
resolveShortIdFetchState: {
|
||||||
|
...defaultFetchState,
|
||||||
|
pending: false,
|
||||||
|
complete: true
|
||||||
|
},
|
||||||
|
dynamicArticleByIdMap: {
|
||||||
|
...state.dynamicArticleByIdMap,
|
||||||
|
[payload.shortId]: payload
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[types.resolveShortIdError]: (state, { payload: error }) => ({
|
||||||
|
...state,
|
||||||
|
resolveShortIdFetchState: {
|
||||||
|
...defaultFetchState,
|
||||||
|
pending: false,
|
||||||
|
errored: true,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
initialState
|
||||||
|
);
|
27
client/src/templates/News/redux/shortId-saga.js
Normal file
27
client/src/templates/News/redux/shortId-saga.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import { getArticleById } from '../../../utils/ajax';
|
||||||
|
import { resolveShortIdComplete, resolveShortIdError } from './';
|
||||||
|
import { handleAPIError, wrapHandledError } from '../../../utils/handled-error';
|
||||||
|
|
||||||
|
function* fetchArticleByIdSaga({ payload }) {
|
||||||
|
try {
|
||||||
|
const { data } = yield call(getArticleById, payload);
|
||||||
|
yield put(resolveShortIdComplete(data));
|
||||||
|
} catch (e) {
|
||||||
|
const { response: { status } = {} } = e;
|
||||||
|
if (typeof status !== 'undefined') {
|
||||||
|
const handledError = wrapHandledError(
|
||||||
|
e,
|
||||||
|
handleAPIError(e, { redirectTo: '/news' })
|
||||||
|
);
|
||||||
|
yield put(resolveShortIdError(handledError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yield put(resolveShortIdError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createShortIdSaga(types) {
|
||||||
|
return [takeEvery(types.resolveShortId, fetchArticleByIdSaga)];
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import qs from 'query-string';
|
||||||
|
|
||||||
const base = '/internal';
|
const base = '/internal';
|
||||||
|
|
||||||
@ -40,8 +41,31 @@ export function getUsernameExists(username) {
|
|||||||
return get(`/api/users/exists?username=${username}`);
|
return get(`/api/users/exists?username=${username}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getArticleById(shortId) {
|
||||||
|
return get(
|
||||||
|
`/n/${shortId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFeaturedList(skip = 0) {
|
||||||
|
return get(
|
||||||
|
`/api/articles?${qs.stringify({
|
||||||
|
filter: JSON.stringify({
|
||||||
|
where: { featured: true, published: true },
|
||||||
|
order: 'firstPublishedDate DESC',
|
||||||
|
limit: 10,
|
||||||
|
skip
|
||||||
|
})
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** POST **/
|
/** POST **/
|
||||||
|
|
||||||
|
export function postPopularityEvent(event) {
|
||||||
|
return post('/p', event);
|
||||||
|
}
|
||||||
|
|
||||||
export function postReportUser(body) {
|
export function postReportUser(body) {
|
||||||
return post('/user/report-user', body);
|
return post('/user/report-user', body);
|
||||||
}
|
}
|
||||||
|
84
client/src/utils/handled-error.js
Normal file
84
client/src/utils/handled-error.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { has } from 'lodash';
|
||||||
|
|
||||||
|
import standardErrorMessage from './standardErrorMessage';
|
||||||
|
import reportedErrorMessage from './reportedErrorMessage';
|
||||||
|
|
||||||
|
import { reportClientSideError } from './report-error';
|
||||||
|
|
||||||
|
export const handledErrorSymbol = Symbol('handledError');
|
||||||
|
|
||||||
|
export function isHandledError(err) {
|
||||||
|
return has(err, handledErrorSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unwrapHandledError(err) {
|
||||||
|
return handledErrorSymbol in err ? err[handledErrorSymbol] : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapHandledError(err, { type, message, redirectTo }) {
|
||||||
|
err[handledErrorSymbol] = { type, message, redirectTo };
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handle400Error(e, options = { redirectTo: '/welcome' }) {
|
||||||
|
const {
|
||||||
|
response: { status }
|
||||||
|
} = e;
|
||||||
|
let { redirectTo } = options;
|
||||||
|
let flash = { ...standardErrorMessage, redirectTo };
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 401:
|
||||||
|
case 403: {
|
||||||
|
return {
|
||||||
|
...flash,
|
||||||
|
type: 'warn',
|
||||||
|
message: 'You are not authorised to continue on this route'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 404: {
|
||||||
|
return {
|
||||||
|
...flash,
|
||||||
|
type: 'info',
|
||||||
|
message:
|
||||||
|
"We couldn't find what you were looking for. " +
|
||||||
|
'Please check and try again'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return flash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handle500Error(
|
||||||
|
e,
|
||||||
|
options = {
|
||||||
|
redirectTo: '/welcome'
|
||||||
|
},
|
||||||
|
_reportClientSideError = reportClientSideError
|
||||||
|
) {
|
||||||
|
const { redirectTo } = options;
|
||||||
|
_reportClientSideError(e, 'We just handled a 5** error on the client');
|
||||||
|
return { ...reportedErrorMessage, redirectTo };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAPIError(
|
||||||
|
e,
|
||||||
|
options,
|
||||||
|
_reportClientSideError = reportClientSideError
|
||||||
|
) {
|
||||||
|
const { response: { status = 0 } = {} } = e;
|
||||||
|
if (status >= 400 && status < 500) {
|
||||||
|
return handle400Error(e, options);
|
||||||
|
}
|
||||||
|
if (status >= 500) {
|
||||||
|
return handle500Error(e, options, _reportClientSideError);
|
||||||
|
}
|
||||||
|
const { redirectTo } = options;
|
||||||
|
_reportClientSideError(
|
||||||
|
e,
|
||||||
|
'We just handled an api error on the client without an error status code'
|
||||||
|
);
|
||||||
|
return { ...reportedErrorMessage, redirectTo };
|
||||||
|
}
|
150
client/src/utils/handled-error.test.js
Normal file
150
client/src/utils/handled-error.test.js
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/* global describe it expect */
|
||||||
|
import { isObject } from 'lodash';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import {
|
||||||
|
isHandledError,
|
||||||
|
wrapHandledError,
|
||||||
|
unwrapHandledError,
|
||||||
|
handledErrorSymbol,
|
||||||
|
handleAPIError
|
||||||
|
} from './handled-error';
|
||||||
|
|
||||||
|
import reportedErrorMessage from './reportedErrorMessage';
|
||||||
|
|
||||||
|
describe('client/src utilities', () => {
|
||||||
|
describe('handled-error.js', () => {
|
||||||
|
const mockHandledErrorData = {
|
||||||
|
type: 'info',
|
||||||
|
message: 'something helpful',
|
||||||
|
redirectTo: '/a-path-we-choose'
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('isHandledError', () => {
|
||||||
|
it('returns a boolean', () => {
|
||||||
|
expect(typeof isHandledError({})).toEqual('boolean');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for an unhandled error', () => {
|
||||||
|
expect(isHandledError(new Error())).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for a handled error', () => {
|
||||||
|
const handledError = new Error();
|
||||||
|
handledError[handledErrorSymbol] = {};
|
||||||
|
|
||||||
|
expect(isHandledError(handledError)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('wrapHandledError', () => {
|
||||||
|
// this is testing implementation details 👎
|
||||||
|
// we need to make these tests more robust 💪
|
||||||
|
it('returns an error with a handledError property', () => {
|
||||||
|
const handledError = wrapHandledError(
|
||||||
|
new Error(),
|
||||||
|
mockHandledErrorData
|
||||||
|
);
|
||||||
|
expect(handledErrorSymbol in handledError).toEqual(true);
|
||||||
|
});
|
||||||
|
it('assigns error handling details to the handledError property', () => {
|
||||||
|
const handledError = wrapHandledError(
|
||||||
|
new Error(),
|
||||||
|
mockHandledErrorData
|
||||||
|
);
|
||||||
|
expect(handledError[handledErrorSymbol]).toEqual(mockHandledErrorData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unwrapHandledError', () => {
|
||||||
|
// this is testing implementation details 👎
|
||||||
|
// we need to make these tests more robust 💪
|
||||||
|
it('returns an object by default', () => {
|
||||||
|
const error = new Error();
|
||||||
|
const unwrappedError = unwrapHandledError(error);
|
||||||
|
expect(isObject(unwrappedError)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the data that was wrapped in the error', () => {
|
||||||
|
const handledError = new Error();
|
||||||
|
handledError[handledErrorSymbol] = mockHandledErrorData;
|
||||||
|
const unwrapped = unwrapHandledError(handledError);
|
||||||
|
expect(unwrapped).toEqual(mockHandledErrorData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleAPIError', () => {
|
||||||
|
let reportMock;
|
||||||
|
beforeEach(() => {
|
||||||
|
reportMock = sinon.spy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns handled error data', () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
const axiosErrorMock = {
|
||||||
|
response: {
|
||||||
|
status: 400
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = handleAPIError(
|
||||||
|
axiosErrorMock,
|
||||||
|
{ redirectTo: '/' },
|
||||||
|
reportMock
|
||||||
|
);
|
||||||
|
expect(result).toHaveProperty('type');
|
||||||
|
expect(result).toHaveProperty('message');
|
||||||
|
expect(result).toHaveProperty('redirectTo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not report 4** errors', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
for (let i = 400; i < 500; i++) {
|
||||||
|
const axiosErrorMock = {
|
||||||
|
response: {
|
||||||
|
status: i
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleAPIError(axiosErrorMock, { redirectTo: '/' }, reportMock);
|
||||||
|
}
|
||||||
|
expect(reportMock.called).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports on 5** errors', () => {
|
||||||
|
const axiosErrorMock = {
|
||||||
|
response: {
|
||||||
|
status: 502
|
||||||
|
}
|
||||||
|
};
|
||||||
|
handleAPIError(axiosErrorMock, { redirectTo: '/' }, reportMock);
|
||||||
|
expect(reportMock.calledOnce).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a `reportedErrorMessage` for a 5** error', () => {
|
||||||
|
const axiosErrorMock = {
|
||||||
|
response: {
|
||||||
|
status: 502
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = handleAPIError(
|
||||||
|
axiosErrorMock,
|
||||||
|
{ redirectTo: '/' },
|
||||||
|
reportMock
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ ...reportedErrorMessage, redirectTo: '/' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects a `null` redirectTo', () => {
|
||||||
|
const axiosErrorMock = {
|
||||||
|
response: {
|
||||||
|
status: 400
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const result = handleAPIError(
|
||||||
|
axiosErrorMock,
|
||||||
|
{ redirectTo: null },
|
||||||
|
reportMock
|
||||||
|
);
|
||||||
|
expect(result.redirectTo).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,22 @@
|
|||||||
|
import { findIndex } from 'lodash';
|
||||||
|
|
||||||
// These regex are not for validation, it is purely to see
|
// These regex are not for validation, it is purely to see
|
||||||
// if we are looking at something like what we want to validate
|
// if we are looking at something like what we want to validate
|
||||||
// before we try to validate
|
// before we try to validate
|
||||||
export const maybeEmailRE = /.*@.*\.\w\w/;
|
export const maybeEmailRE = /.*@.*\.\w\w/;
|
||||||
export const maybeUrlRE = /https?:\/\/.*\..*/;
|
export const maybeUrlRE = /https?:\/\/.*\..*/;
|
||||||
export const hasProtocolRE = /^http/;
|
export const hasProtocolRE = /^http/;
|
||||||
|
|
||||||
|
export const getShortIdFromSlug = (slug = '') => {
|
||||||
|
const slugToArray = [...slug];
|
||||||
|
const endOfUseableSlug = findIndex(
|
||||||
|
slugToArray,
|
||||||
|
char => char === '?' || char === '#'
|
||||||
|
);
|
||||||
|
let operableSlug = slug.slice(0);
|
||||||
|
if (endOfUseableSlug !== -1) {
|
||||||
|
operableSlug = slug.slice(0, endOfUseableSlug);
|
||||||
|
}
|
||||||
|
const [, maybeShortId = ''] = operableSlug.split('--');
|
||||||
|
return maybeShortId.replace(/\/*$/, '');
|
||||||
|
};
|
||||||
|
7
client/src/utils/report-error.js
Normal file
7
client/src/utils/report-error.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/* global Rollbar ENVIRONMENT */
|
||||||
|
|
||||||
|
export function reportClientSideError(e, message = 'Unhandled error') {
|
||||||
|
return ENVIRONMENT === 'production'
|
||||||
|
? Rollbar.error(`Client: ${message}`, e)
|
||||||
|
: console.error(`Stub Rollbar call - Client: ${message}`, e);
|
||||||
|
}
|
6
client/src/utils/reportedErrorMessage.js
Normal file
6
client/src/utils/reportedErrorMessage.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
type: 'danger',
|
||||||
|
message:
|
||||||
|
'Something is not quite right. A report has been generated and the ' +
|
||||||
|
'freeCodeCamp.org team have been notified.'
|
||||||
|
};
|
45
client/src/utils/utils.test.js
Normal file
45
client/src/utils/utils.test.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/* global describe it expect */
|
||||||
|
import {
|
||||||
|
slugWithId,
|
||||||
|
slugWithIdAndHash,
|
||||||
|
slugWithIdAndQuery,
|
||||||
|
slugWithIdAndTrailingSlash,
|
||||||
|
slugWithoutId,
|
||||||
|
mockId
|
||||||
|
} from '../__mocks__/news-article';
|
||||||
|
|
||||||
|
import { getShortIdFromSlug } from './';
|
||||||
|
|
||||||
|
describe('client/src utilities', () => {
|
||||||
|
describe('getShortIdFromSlug', () => {
|
||||||
|
const emptyString = '';
|
||||||
|
it('returns a string', () => {
|
||||||
|
expect(typeof getShortIdFromSlug()).toEqual('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts the shortId when one is present', () => {
|
||||||
|
const result = getShortIdFromSlug(slugWithId);
|
||||||
|
expect(result).toEqual(mockId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still returns a string when no id is found', () => {
|
||||||
|
const result = getShortIdFromSlug(slugWithoutId);
|
||||||
|
expect(result).toEqual(emptyString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle query', () => {
|
||||||
|
const result = getShortIdFromSlug(slugWithIdAndQuery);
|
||||||
|
expect(result).toEqual(mockId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle hash', () => {
|
||||||
|
const result = getShortIdFromSlug(slugWithIdAndHash);
|
||||||
|
expect(result).toEqual(mockId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle trails slashes', () => {
|
||||||
|
const result = getShortIdFromSlug(slugWithIdAndTrailingSlash);
|
||||||
|
expect(result).toEqual(mockId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
114
client/utils/gatsby/challengePageCreator.js
Normal file
114
client/utils/gatsby/challengePageCreator.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const { dasherize } = require('..');
|
||||||
|
|
||||||
|
const { viewTypes } = require('../challengeTypes');
|
||||||
|
|
||||||
|
const backend = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/templates/Challenges/backend/Show.js'
|
||||||
|
);
|
||||||
|
const classic = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/templates/Challenges/classic/Show.js'
|
||||||
|
);
|
||||||
|
const project = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/templates/Challenges/project/Show.js'
|
||||||
|
);
|
||||||
|
const intro = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/templates/Introduction/Intro.js'
|
||||||
|
);
|
||||||
|
const superBlockIntro = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/templates/Introduction/SuperBlockIntro.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
const views = {
|
||||||
|
backend,
|
||||||
|
classic,
|
||||||
|
modern: classic,
|
||||||
|
project
|
||||||
|
// quiz: Quiz
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextChallengePath = (node, index, nodeArray) => {
|
||||||
|
const next = nodeArray[index + 1];
|
||||||
|
return next ? next.node.fields.slug : '/';
|
||||||
|
};
|
||||||
|
const getTemplateComponent = challengeType => views[viewTypes[challengeType]];
|
||||||
|
|
||||||
|
const getIntroIfRequired = (node, index, nodeArray) => {
|
||||||
|
const next = nodeArray[index + 1];
|
||||||
|
const isEndOfBlock = next && next.node.challengeOrder === 0;
|
||||||
|
let nextSuperBlock = '';
|
||||||
|
let nextBlock = '';
|
||||||
|
if (next) {
|
||||||
|
const { superBlock, block } = next.node;
|
||||||
|
nextSuperBlock = superBlock;
|
||||||
|
nextBlock = block;
|
||||||
|
}
|
||||||
|
return isEndOfBlock
|
||||||
|
? `/learn/${dasherize(nextSuperBlock)}/${dasherize(nextBlock)}`
|
||||||
|
: '';
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
|
||||||
|
const {
|
||||||
|
fields: { slug },
|
||||||
|
required = [],
|
||||||
|
template,
|
||||||
|
challengeType,
|
||||||
|
id
|
||||||
|
} = node;
|
||||||
|
if (challengeType === 7) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPage({
|
||||||
|
path: slug,
|
||||||
|
component: getTemplateComponent(challengeType),
|
||||||
|
context: {
|
||||||
|
challengeMeta: {
|
||||||
|
introPath: getIntroIfRequired(node, index, thisArray),
|
||||||
|
template,
|
||||||
|
required,
|
||||||
|
nextChallengePath: getNextChallengePath(node, index, thisArray),
|
||||||
|
id
|
||||||
|
},
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createBlockIntroPages = createPage => edge => {
|
||||||
|
const {
|
||||||
|
fields: { slug },
|
||||||
|
frontmatter: { block }
|
||||||
|
} = edge.node;
|
||||||
|
|
||||||
|
return createPage({
|
||||||
|
path: slug,
|
||||||
|
component: intro,
|
||||||
|
context: {
|
||||||
|
block: dasherize(block),
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.createSuperBlockIntroPages = createPage => edge => {
|
||||||
|
const {
|
||||||
|
fields: { slug },
|
||||||
|
frontmatter: { superBlock }
|
||||||
|
} = edge.node;
|
||||||
|
|
||||||
|
return createPage({
|
||||||
|
path: slug,
|
||||||
|
component: superBlockIntro,
|
||||||
|
context: {
|
||||||
|
superBlock: dasherize(superBlock),
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
41
client/utils/gatsby/guidePageCreator.js
Normal file
41
client/utils/gatsby/guidePageCreator.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const select = require('unist-util-select');
|
||||||
|
const { head } = require('lodash');
|
||||||
|
|
||||||
|
const { isAStubRE } = require('../regEx');
|
||||||
|
|
||||||
|
const guideArticle = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../src/templates/Guide/GuideArticle.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
exports.createGuideArticlePages = createPage => ({
|
||||||
|
node: {
|
||||||
|
htmlAst,
|
||||||
|
excerpt,
|
||||||
|
fields: { slug },
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
let meta = {};
|
||||||
|
|
||||||
|
if (!isAStubRE.test(excerpt)) {
|
||||||
|
const featureImage = head(select(htmlAst, 'element[tagName=img]'));
|
||||||
|
meta.featureImage = featureImage
|
||||||
|
? featureImage.properties.src
|
||||||
|
: 'https://s3.amazonaws.com/freecodecamp' +
|
||||||
|
'/reecodecamp-square-logo-large.jpg';
|
||||||
|
|
||||||
|
const description = head(select(htmlAst, 'element[tagName=p]'));
|
||||||
|
meta.description = description ? description.children[0].value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPage({
|
||||||
|
path: `/guide${slug}`,
|
||||||
|
component: guideArticle,
|
||||||
|
context: {
|
||||||
|
id,
|
||||||
|
meta
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -1,154 +1,9 @@
|
|||||||
const path = require('path');
|
const challengePageCreators = require('./challengePageCreator');
|
||||||
const select = require('unist-util-select');
|
const guidePageCreators = require('./guidePageCreator');
|
||||||
const { head } = require('lodash');
|
const newsPageCreators = require('./newsPageCreator');
|
||||||
|
|
||||||
const { dasherize } = require('..');
|
module.exports = {
|
||||||
const { isAStubRE } = require('../regEx');
|
...challengePageCreators,
|
||||||
|
...guidePageCreators,
|
||||||
const { viewTypes } = require('../challengeTypes');
|
...newsPageCreators
|
||||||
|
|
||||||
const backend = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../src/templates/Challenges/backend/Show.js'
|
|
||||||
);
|
|
||||||
const classic = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../src/templates/Challenges/classic/Show.js'
|
|
||||||
);
|
|
||||||
const project = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../src/templates/Challenges/project/Show.js'
|
|
||||||
);
|
|
||||||
const intro = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../src/templates/Introduction/Intro.js'
|
|
||||||
);
|
|
||||||
const superBlockIntro = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../src/templates/Introduction/SuperBlockIntro.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
const guideArticle = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../src/templates/Guide/GuideArticle.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
const views = {
|
|
||||||
backend,
|
|
||||||
classic,
|
|
||||||
modern: classic,
|
|
||||||
project
|
|
||||||
// quiz: Quiz
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNextChallengePath = (node, index, nodeArray) => {
|
|
||||||
const next = nodeArray[index + 1];
|
|
||||||
return next ? next.node.fields.slug : '/';
|
|
||||||
};
|
|
||||||
const getTemplateComponent = challengeType => views[viewTypes[challengeType]];
|
|
||||||
|
|
||||||
const getIntroIfRequired = (node, index, nodeArray) => {
|
|
||||||
const next = nodeArray[index + 1];
|
|
||||||
const isEndOfBlock = next && next.node.challengeOrder === 0;
|
|
||||||
let nextSuperBlock = '';
|
|
||||||
let nextBlock = '';
|
|
||||||
if (next) {
|
|
||||||
const { superBlock, block } = next.node;
|
|
||||||
nextSuperBlock = superBlock;
|
|
||||||
nextBlock = block;
|
|
||||||
}
|
|
||||||
return isEndOfBlock
|
|
||||||
? `/learn/${dasherize(nextSuperBlock)}/${dasherize(nextBlock)}`
|
|
||||||
: '';
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
|
|
||||||
const {
|
|
||||||
fields: { slug },
|
|
||||||
required = [],
|
|
||||||
template,
|
|
||||||
challengeType,
|
|
||||||
id
|
|
||||||
} = node;
|
|
||||||
if (challengeType === 7) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createPage({
|
|
||||||
path: slug,
|
|
||||||
component: getTemplateComponent(challengeType),
|
|
||||||
context: {
|
|
||||||
challengeMeta: {
|
|
||||||
introPath: getIntroIfRequired(node, index, thisArray),
|
|
||||||
template,
|
|
||||||
required,
|
|
||||||
nextChallengePath: getNextChallengePath(node, index, thisArray),
|
|
||||||
id
|
|
||||||
},
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createBlockIntroPages = createPage => edge => {
|
|
||||||
const {
|
|
||||||
fields: { slug },
|
|
||||||
frontmatter: { block }
|
|
||||||
} = edge.node;
|
|
||||||
|
|
||||||
return createPage({
|
|
||||||
path: slug,
|
|
||||||
component: intro,
|
|
||||||
context: {
|
|
||||||
block: dasherize(block),
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createSuperBlockIntroPages = createPage => edge => {
|
|
||||||
const {
|
|
||||||
fields: { slug },
|
|
||||||
frontmatter: { superBlock }
|
|
||||||
} = edge.node;
|
|
||||||
|
|
||||||
return createPage({
|
|
||||||
path: slug,
|
|
||||||
component: superBlockIntro,
|
|
||||||
context: {
|
|
||||||
superBlock: dasherize(superBlock),
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createGuideArticlePages = createPage => ({
|
|
||||||
node: {
|
|
||||||
htmlAst,
|
|
||||||
excerpt,
|
|
||||||
fields: { slug },
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}) => {
|
|
||||||
let meta = {};
|
|
||||||
|
|
||||||
if (!isAStubRE.test(excerpt)) {
|
|
||||||
const featureImage = head(select(htmlAst, 'element[tagName=img]'));
|
|
||||||
meta.featureImage = featureImage
|
|
||||||
? featureImage.properties.src
|
|
||||||
: 'https://s3.amazonaws.com/freecodecamp' +
|
|
||||||
'/reecodecamp-square-logo-large.jpg';
|
|
||||||
|
|
||||||
const description = head(select(htmlAst, 'element[tagName=p]'));
|
|
||||||
meta.description = description ? description.children[0].value : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return createPage({
|
|
||||||
path: `/guide${slug}`,
|
|
||||||
component: guideArticle,
|
|
||||||
context: {
|
|
||||||
id,
|
|
||||||
meta
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
22
client/utils/gatsby/newsPageCreator.js
Normal file
22
client/utils/gatsby/newsPageCreator.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const newsArticle = path.resolve(
|
||||||
|
__dirname, '../../src/templates/News/ShowArticle/index.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
exports.createNewsArticle = createPage => ({
|
||||||
|
node: {
|
||||||
|
fields: { slug },
|
||||||
|
shortId,
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}) =>
|
||||||
|
createPage({
|
||||||
|
path: slug,
|
||||||
|
component: newsArticle,
|
||||||
|
context: {
|
||||||
|
slug,
|
||||||
|
shortId,
|
||||||
|
id
|
||||||
|
}
|
||||||
|
});
|
18
client/utils/news.js
Normal file
18
client/utils/news.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
exports.createArticleSlug = ({
|
||||||
|
username = '',
|
||||||
|
slugPart = '',
|
||||||
|
shortId = ''
|
||||||
|
} = {}) => {
|
||||||
|
if (!username || !slugPart || !shortId) {
|
||||||
|
throw new Error(`
|
||||||
|
createArtcileSlug: One or more properties were missing, all are required
|
||||||
|
|
||||||
|
{
|
||||||
|
username: ${username},
|
||||||
|
slugPart: ${slugPart},
|
||||||
|
shortId: ${shortId}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
return `/news/${username}/${slugPart.concat('--', shortId)}`;
|
||||||
|
};
|
32
client/utils/news.test.js
Normal file
32
client/utils/news.test.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* global describe it expect */
|
||||||
|
import { mockArguments, slugWithId} from '../src/__mocks__/news-article';
|
||||||
|
import { createArticleSlug } from './news';
|
||||||
|
|
||||||
|
describe('news utils', () => {
|
||||||
|
describe('createArticleSlug', () => {
|
||||||
|
|
||||||
|
it('returns a string', () => {
|
||||||
|
expect(typeof createArticleSlug(mockArguments)).toEqual('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when values are missing', () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
/* eslint-disable no-undefined */
|
||||||
|
expect(() =>
|
||||||
|
createArticleSlug({ ...mockArguments, shortId: undefined })
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
createArticleSlug({ ...mockArguments, slugPart: undefined })
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
createArticleSlug({ ...mockArguments, username: undefined })
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a slug in the expected format', () => {
|
||||||
|
const result = createArticleSlug(mockArguments);
|
||||||
|
|
||||||
|
expect(result).toEqual(slugWithId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -27,6 +27,9 @@ const {
|
|||||||
TWITTER_TOKEN,
|
TWITTER_TOKEN,
|
||||||
TWITTER_TOKEN_SECRET,
|
TWITTER_TOKEN_SECRET,
|
||||||
|
|
||||||
|
ROLLBAR_APP_ID,
|
||||||
|
ROLLBAR_CLIENT_ID,
|
||||||
|
|
||||||
STRIPE_PUBLIC,
|
STRIPE_PUBLIC,
|
||||||
STRIPE_SECRET
|
STRIPE_SECRET
|
||||||
} = process.env;
|
} = process.env;
|
||||||
@ -83,6 +86,11 @@ module.exports = {
|
|||||||
passReqToCallback: true
|
passReqToCallback: true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
rollbar: {
|
||||||
|
appId: ROLLBAR_APP_ID,
|
||||||
|
clientId: ROLLBAR_CLIENT_ID
|
||||||
|
},
|
||||||
|
|
||||||
stripe: {
|
stripe: {
|
||||||
public: STRIPE_PUBLIC,
|
public: STRIPE_PUBLIC,
|
||||||
secret: STRIPE_SECRET
|
secret: STRIPE_SECRET
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
"api-server",
|
"api-server",
|
||||||
"client",
|
"client",
|
||||||
|
"client/plugins/*",
|
||||||
"curriculum",
|
"curriculum",
|
||||||
"tools/challenge-md-parser",
|
"tools/challenge-md-parser",
|
||||||
"tools/scripts/seed"
|
"tools/scripts/seed"
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {BrowserRouter} from 'react-router-dom';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
|
|
||||||
import NewsApp from './NewsApp';
|
|
||||||
|
|
||||||
const newsMountPoint = document.getElementById('news-app-mount');
|
|
||||||
|
|
||||||
const App = (
|
|
||||||
<BrowserRouter basename='/news'>
|
|
||||||
<NewsApp />
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
render(
|
|
||||||
App,
|
|
||||||
newsMountPoint
|
|
||||||
);
|
|
@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Media from 'react-media';
|
|
||||||
import { Col, Navbar, Row } from 'react-bootstrap';
|
|
||||||
import FCCSearchBar from 'react-freecodecamp-search';
|
|
||||||
import NavLogo from './components/NavLogo';
|
|
||||||
import NavLinks from './components/NavLinks';
|
|
||||||
|
|
||||||
import propTypes from './navPropTypes';
|
|
||||||
|
|
||||||
function LargeNav({ clickOnLogo }) {
|
|
||||||
return (
|
|
||||||
<Media
|
|
||||||
query='(min-width: 956px)'
|
|
||||||
render={
|
|
||||||
() => (
|
|
||||||
<Row>
|
|
||||||
<Col className='nav-component' sm={ 7 } xs={ 12 }>
|
|
||||||
<Navbar.Header>
|
|
||||||
<NavLogo clickOnLogo={ clickOnLogo } />
|
|
||||||
<FCCSearchBar />
|
|
||||||
</Navbar.Header>
|
|
||||||
</Col>
|
|
||||||
<Col className='nav-component nav-links' sm={ 5 } xs={ 0 }>
|
|
||||||
<Navbar.Collapse>
|
|
||||||
<NavLinks />
|
|
||||||
</Navbar.Collapse>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LargeNav.displayName = 'LargeNav';
|
|
||||||
LargeNav.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default LargeNav;
|
|
@ -1,42 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Media from 'react-media';
|
|
||||||
import { Navbar, Row } from 'react-bootstrap';
|
|
||||||
import FCCSearchBar from 'react-freecodecamp-search';
|
|
||||||
import NavLogo from './components/NavLogo';
|
|
||||||
import NavLinks from './components/NavLinks';
|
|
||||||
import propTypes from './navPropTypes';
|
|
||||||
|
|
||||||
function MediumNav({ clickOnLogo }) {
|
|
||||||
return (
|
|
||||||
<Media
|
|
||||||
query={{ maxWidth: 955, minWidth: 751 }}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
matches => matches && typeof window !== 'undefined' && (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Navbar.Header className='medium-nav'>
|
|
||||||
<div className='nav-component header'>
|
|
||||||
<Navbar.Toggle />
|
|
||||||
<NavLogo clickOnLogo={ clickOnLogo } />
|
|
||||||
<FCCSearchBar />
|
|
||||||
</div>
|
|
||||||
<div className='nav-component bins'/>
|
|
||||||
</Navbar.Header>
|
|
||||||
</Row>
|
|
||||||
<Row className='collapse-row'>
|
|
||||||
<Navbar.Collapse>
|
|
||||||
<NavLinks />
|
|
||||||
</Navbar.Collapse>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Media>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MediumNav.displayName = 'MediumNav';
|
|
||||||
MediumNav.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default MediumNav;
|
|
@ -1,31 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Navbar } from 'react-bootstrap';
|
|
||||||
|
|
||||||
import LargeNav from './LargeNav';
|
|
||||||
import MediumNav from './MediumNav';
|
|
||||||
import SmallNav from './SmallNav';
|
|
||||||
|
|
||||||
const allNavs = [
|
|
||||||
LargeNav,
|
|
||||||
MediumNav,
|
|
||||||
SmallNav
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function FCCNav() {
|
|
||||||
const withNavProps = Component => (
|
|
||||||
<Component
|
|
||||||
key={ Component.displayName }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Navbar
|
|
||||||
className='nav-height'
|
|
||||||
id='navbar'
|
|
||||||
staticTop={ true }
|
|
||||||
>
|
|
||||||
{
|
|
||||||
allNavs.map(withNavProps)
|
|
||||||
}
|
|
||||||
</Navbar>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Media from 'react-media';
|
|
||||||
import { Navbar, Row } from 'react-bootstrap';
|
|
||||||
import FCCSearchBar from 'react-freecodecamp-search';
|
|
||||||
import NavLogo from './components/NavLogo';
|
|
||||||
import NavLinks from './components/NavLinks';
|
|
||||||
|
|
||||||
import propTypes from './navPropTypes';
|
|
||||||
|
|
||||||
function SmallNav({ clickOnLogo }) {
|
|
||||||
return (
|
|
||||||
<Media
|
|
||||||
query='(max-width: 750px)'
|
|
||||||
>
|
|
||||||
{
|
|
||||||
matches => matches && typeof window !== 'undefined' && (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Navbar.Header className='small-nav'>
|
|
||||||
<div className='nav-component header'>
|
|
||||||
<Navbar.Toggle />
|
|
||||||
<NavLogo clickOnLogo={ clickOnLogo } />
|
|
||||||
</div>
|
|
||||||
<div className='nav-component bins'/>
|
|
||||||
</Navbar.Header>
|
|
||||||
</Row>
|
|
||||||
<Row className='collapse-row'>
|
|
||||||
<Navbar.Collapse>
|
|
||||||
<NavLinks>
|
|
||||||
<FCCSearchBar />
|
|
||||||
</NavLinks>
|
|
||||||
</Navbar.Collapse>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Media>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SmallNav.displayName = 'SmallNav';
|
|
||||||
SmallNav.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default SmallNav;
|
|
@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { NavItem, Nav } from 'react-bootstrap';
|
|
||||||
import { startCase } from 'lodash';
|
|
||||||
|
|
||||||
const urls = {
|
|
||||||
curriculum: 'https://learn.freecodecamp.org',
|
|
||||||
forum: 'https://forum.freecodecamp.org',
|
|
||||||
news: 'https://freecodecamp.org/news'
|
|
||||||
};
|
|
||||||
|
|
||||||
const Links = Object.keys(urls).map(key => (
|
|
||||||
<NavItem href={urls[key]} key={key} target='_blank'>
|
|
||||||
{startCase(key)}
|
|
||||||
</NavItem>
|
|
||||||
));
|
|
||||||
const propTypes = {};
|
|
||||||
|
|
||||||
function NavLinks() {
|
|
||||||
return (
|
|
||||||
<Nav id='nav-links' navbar={true} pullRight={true}>
|
|
||||||
{Links}
|
|
||||||
<NavItem href='/settings'>Settings</NavItem>
|
|
||||||
</Nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NavLinks.displayName = 'NavLinks';
|
|
||||||
NavLinks.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default NavLinks;
|
|
||||||
|
|
||||||
/*
|
|
||||||
<SignUp
|
|
||||||
isInDropDown={ !isInNav }
|
|
||||||
showLoading={ showLoading }
|
|
||||||
showSignUp={ !isSignedIn }
|
|
||||||
/>
|
|
||||||
*/
|
|
@ -1,41 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { NavbarBrand } from 'react-bootstrap';
|
|
||||||
import Media from 'react-media';
|
|
||||||
|
|
||||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
|
||||||
const fCCglyph = 'https://s3.amazonaws.com/freecodecamp/FFCFire.png';
|
|
||||||
|
|
||||||
const propTypes = {};
|
|
||||||
|
|
||||||
function NavLogo() {
|
|
||||||
return (
|
|
||||||
<NavbarBrand>
|
|
||||||
<a
|
|
||||||
href='/'
|
|
||||||
>
|
|
||||||
<Media query='(min-width: 350px)'>
|
|
||||||
{
|
|
||||||
matches => matches ? (
|
|
||||||
<img
|
|
||||||
alt='learn to code javascript at freeCodeCamp logo'
|
|
||||||
className='nav-logo logo'
|
|
||||||
src={ fCClogo }
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
alt='learn to code javascript at freeCodeCamp logo'
|
|
||||||
className='nav-logo logo'
|
|
||||||
src={ fCCglyph }
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Media>
|
|
||||||
</a>
|
|
||||||
</NavbarBrand>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NavLogo.displayName = 'NavLogo';
|
|
||||||
NavLogo.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default NavLogo;
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './Nav';
|
|
@ -1,3 +0,0 @@
|
|||||||
// import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
export default {};
|
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const propTypes = {};
|
|
||||||
|
|
||||||
function NewArticle() {
|
|
||||||
return (
|
|
||||||
<h2>New Article</h2>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NewArticle.displayName = 'NewArticle';
|
|
||||||
NewArticle.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default NewArticle;
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './EditArticle';
|
|
@ -1 +0,0 @@
|
|||||||
// no-op
|
|
@ -1,166 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import { Image } from 'react-bootstrap';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
|
|
||||||
import { getFeaturedList } from '../../utils/ajax';
|
|
||||||
import { Loader, Spacer } from '../../../common/app/helperComponents';
|
|
||||||
import BannerWide from '../../components/BannerWide';
|
|
||||||
import ArticleMeta from '../../components/ArticleMeta';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
history: PropTypes.shape({
|
|
||||||
push: PropTypes.func.isRequired
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = `
|
|
||||||
.featured-list {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list-item {
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list-item .title {
|
|
||||||
color: #333;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list-item a {
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list-image {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list-item a:hover,
|
|
||||||
.featured-list-item a:focus {
|
|
||||||
text-decoration: none;
|
|
||||||
text-decoration-line: none;
|
|
||||||
text-decoration-color: transparaent;
|
|
||||||
}
|
|
||||||
.featured-list-item a:hover > .meta-wrapper,
|
|
||||||
.featured-list-item a:focus > .meta-wrapper {
|
|
||||||
color: #006400;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
class Featured extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
fetchState: {
|
|
||||||
pending: false,
|
|
||||||
complete: false,
|
|
||||||
errored: false,
|
|
||||||
error: null
|
|
||||||
},
|
|
||||||
featuredList: []
|
|
||||||
};
|
|
||||||
|
|
||||||
this.fetchFeaturedList = this.fetchFeaturedList.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
return this.fetchFeaturedList();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFeaturedList() {
|
|
||||||
return this.setState(
|
|
||||||
{
|
|
||||||
fetchState: { pending: true, complete: false, errored: false }
|
|
||||||
},
|
|
||||||
() =>
|
|
||||||
getFeaturedList().then(({ data }) =>
|
|
||||||
this.setState({
|
|
||||||
featuredList: data,
|
|
||||||
fetchState: {
|
|
||||||
pending: false,
|
|
||||||
complete: true,
|
|
||||||
errored: false,
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createHandleArticleClick(slug, article) {
|
|
||||||
const { history } = this.props;
|
|
||||||
return e => {
|
|
||||||
e.preventDefault();
|
|
||||||
return history.push({ pathname: slug, state: { article } });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFeatured(articles) {
|
|
||||||
return articles.map(article => {
|
|
||||||
const slug = `/${article.author.username}/`.concat(
|
|
||||||
article.slugPart,
|
|
||||||
'--',
|
|
||||||
article.shortId
|
|
||||||
);
|
|
||||||
const { featureImage, shortId, title } = article;
|
|
||||||
return (
|
|
||||||
<li className='featured-list-item' key={shortId}>
|
|
||||||
<a
|
|
||||||
href={'/news' + slug}
|
|
||||||
onClick={this.createHandleArticleClick(slug, article)}
|
|
||||||
>
|
|
||||||
<h3 className='title'>{title}</h3>
|
|
||||||
{featureImage && featureImage.src ? (
|
|
||||||
<Image
|
|
||||||
className='featured-list-image'
|
|
||||||
responsive={true}
|
|
||||||
src={featureImage.src}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<BannerWide />
|
|
||||||
)}
|
|
||||||
<ArticleMeta article={article} />
|
|
||||||
</a>
|
|
||||||
<Spacer />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
fetchState: { pending, complete, errored },
|
|
||||||
featuredList
|
|
||||||
} = this.state;
|
|
||||||
if (pending || !complete) {
|
|
||||||
return (
|
|
||||||
<div className='full-size' style={{ position: 'fixed', left: 0 }}>
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (complete && errored) {
|
|
||||||
return <h2>Oh noes!! Something went wrong!</h2>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Helmet>
|
|
||||||
<style>{styles}</style>
|
|
||||||
<title>Featured | freeCodeCamp News</title>
|
|
||||||
</Helmet>
|
|
||||||
<ul className='featured-list'>{this.renderFeatured(featuredList)}</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Featured.displayName = 'Featured';
|
|
||||||
Featured.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default withRouter(Featured);
|
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const propTypes = {};
|
|
||||||
|
|
||||||
function Latest() {
|
|
||||||
return (
|
|
||||||
<h2>Latest</h2>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Latest.displayName = 'Latest';
|
|
||||||
Latest.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default Latest;
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './Latest';
|
|
@ -1,237 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
import Youtube from 'react-youtube';
|
|
||||||
import { Image } from 'react-bootstrap';
|
|
||||||
|
|
||||||
import Author from './components/Author';
|
|
||||||
import { Loader, Spacer } from '../../../common/app/helperComponents';
|
|
||||||
import { getArticleById, postPopularityEvent } from '../../utils/ajax';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
history: PropTypes.shape({
|
|
||||||
push: PropTypes.func.isRequired
|
|
||||||
}),
|
|
||||||
location: PropTypes.shape({
|
|
||||||
state: PropTypes.object,
|
|
||||||
pathname: PropTypes.string
|
|
||||||
}),
|
|
||||||
match: PropTypes.shape({
|
|
||||||
params: PropTypes.shape({
|
|
||||||
username: PropTypes.string,
|
|
||||||
slug: PropTypes.string
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const youtubeOpts = {
|
|
||||||
playerVars: {
|
|
||||||
// https://developers.google.com/youtube/player_parameters
|
|
||||||
autoplay: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = `
|
|
||||||
|
|
||||||
.show-article figure {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-article figcaption > * {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-article figcaption {
|
|
||||||
padding-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-article a {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-image-wrapper {
|
|
||||||
padding-top: 32px;
|
|
||||||
}
|
|
||||||
.youtube-wrapper {
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 56.25%; /* 16:9 */
|
|
||||||
padding-top: 25px;
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.youtube-wrapper iframe {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 95%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
class ShowArticle extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
fetchState: {
|
|
||||||
pending: false,
|
|
||||||
complete: false,
|
|
||||||
errored: false,
|
|
||||||
error: null
|
|
||||||
},
|
|
||||||
currentArticle: {},
|
|
||||||
requiredArticle: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
this.fetchArticle = this.fetchArticle.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
const {
|
|
||||||
history,
|
|
||||||
match: {
|
|
||||||
params: { username, slug }
|
|
||||||
},
|
|
||||||
location: { state: { article } = {} }
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (username && !slug) {
|
|
||||||
return history.push('/');
|
|
||||||
}
|
|
||||||
const [, shortId] = slug.split('--');
|
|
||||||
postPopularityEvent({
|
|
||||||
event: 'view',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
shortId
|
|
||||||
});
|
|
||||||
if (article) {
|
|
||||||
/* eslint-disable react/no-did-mount-set-state */
|
|
||||||
return this.setState(
|
|
||||||
{
|
|
||||||
fetchState: {
|
|
||||||
complete: true
|
|
||||||
},
|
|
||||||
currentArticle: article,
|
|
||||||
requiredArticle: shortId
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
window.location.state = null;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.fetchArticle();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchArticle() {
|
|
||||||
const {
|
|
||||||
match: {
|
|
||||||
params: { slug }
|
|
||||||
}
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const [, shortId] = slug.split('--');
|
|
||||||
return this.setState(
|
|
||||||
{
|
|
||||||
requiredArticle: shortId,
|
|
||||||
fetchState: { pending: true, complete: false, errored: false }
|
|
||||||
},
|
|
||||||
() =>
|
|
||||||
getArticleById(shortId)
|
|
||||||
.then(({ data }) =>
|
|
||||||
this.setState({
|
|
||||||
currentArticle: data,
|
|
||||||
fetchState: {
|
|
||||||
pending: false,
|
|
||||||
complete: true,
|
|
||||||
errored: false,
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(console.error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
youtubeReady(event) {
|
|
||||||
event.target.pauseVideo();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
fetchState: { pending, complete, errored },
|
|
||||||
currentArticle: { title, renderableContent, youtubeId, featureImage },
|
|
||||||
currentArticle
|
|
||||||
} = this.state;
|
|
||||||
if (pending || !complete) {
|
|
||||||
return (
|
|
||||||
<div className='full-size' style={{ position: 'fixed', left: 0 }}>
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (complete && errored) {
|
|
||||||
return <h2>Oh noes!! Something went wrong!</h2>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegEx finds the first paragraph and groups the content
|
|
||||||
const description = renderableContent.match(/<p>(.*?)<\/p>/)[1];
|
|
||||||
const slug = this.props.location.pathname;
|
|
||||||
return (
|
|
||||||
<article className='show-article'>
|
|
||||||
<Helmet>
|
|
||||||
<style>{styles}</style>
|
|
||||||
<title>{`${title} | freeCodeCamp News`}</title>
|
|
||||||
<link
|
|
||||||
href={`https://www.freecodecamp.org/news${slug}`}
|
|
||||||
rel='canonical'
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
content={`https://www.freecodecamp.org/news${slug}`}
|
|
||||||
property='og:url'
|
|
||||||
/>
|
|
||||||
<meta content={title} property='og:title' />
|
|
||||||
<meta content={description} property='og:description' />
|
|
||||||
<meta content={description} name='description' />
|
|
||||||
<meta content={featureImage.src} property='og:image' />
|
|
||||||
</Helmet>
|
|
||||||
<Author article={currentArticle} />
|
|
||||||
<h2>{title}</h2>
|
|
||||||
<div className='feature-image-wrapper'>
|
|
||||||
<figure>
|
|
||||||
<Image
|
|
||||||
alt={featureImage.alt}
|
|
||||||
responsive={true}
|
|
||||||
src={featureImage.src}
|
|
||||||
/>
|
|
||||||
{featureImage.caption ? (
|
|
||||||
<figcaption
|
|
||||||
dangerouslySetInnerHTML={{ __html: featureImage.caption }}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
<Spacer />
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: renderableContent }} />
|
|
||||||
<div className='youtube-wrapper'>
|
|
||||||
{youtubeId ? (
|
|
||||||
<Youtube
|
|
||||||
onReady={this.youtubeReady}
|
|
||||||
opts={youtubeOpts}
|
|
||||||
videoId={youtubeId}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<Spacer />
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShowArticle.displayName = 'ShowArticle';
|
|
||||||
ShowArticle.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default withRouter(ShowArticle);
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './Show';
|
|
@ -1,13 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Route, Redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Show from './Show';
|
|
||||||
import Featured from './Featured';
|
|
||||||
|
|
||||||
export const routes = [
|
|
||||||
<Route component={Featured} exact={true} path='/' />,
|
|
||||||
<Route exact={true} path='/:username' render={() => <Redirect to='/' />} />,
|
|
||||||
<Route component={Show} path='/:username/:slug' />
|
|
||||||
].map(el => ({ ...el, key: el.props.path }));
|
|
||||||
|
|
||||||
// <Route component={EditArticle} path='/new' />
|
|
@ -1,27 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import qs from 'query-string';
|
|
||||||
|
|
||||||
export function getArticleById(shortId) {
|
|
||||||
return axios.get(
|
|
||||||
`/api/articles/findOne?${qs.stringify({
|
|
||||||
filter: JSON.stringify({ where: { shortId } })
|
|
||||||
})}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFeaturedList(skip = 0) {
|
|
||||||
return axios.get(
|
|
||||||
`/api/articles?${qs.stringify({
|
|
||||||
filter: JSON.stringify({
|
|
||||||
where: { featured: true, published: true },
|
|
||||||
order: 'firstPublishedDate DESC',
|
|
||||||
limit: 10,
|
|
||||||
skip
|
|
||||||
})
|
|
||||||
})}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function postPopularityEvent(event) {
|
|
||||||
return axios.post('/p', event);
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ COMPOSE_PROJECT_NAME=freecodecamp
|
|||||||
MONGOHQ_URL='mongodb://localhost:27017/freecodecamp'
|
MONGOHQ_URL='mongodb://localhost:27017/freecodecamp'
|
||||||
|
|
||||||
ROLLBAR_APP_ID='my-rollbar-app-id'
|
ROLLBAR_APP_ID='my-rollbar-app-id'
|
||||||
|
ROLLBAR_CLIENT_ID='post_client_id from rollbar dashboard'
|
||||||
|
|
||||||
AUTH0_CLIENT_ID=stuff
|
AUTH0_CLIENT_ID=stuff
|
||||||
AUTH0_CLIENT_SECRET=stuff
|
AUTH0_CLIENT_SECRET=stuff
|
||||||
|
@ -2,13 +2,19 @@ const path = require('path');
|
|||||||
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
|
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
|
||||||
const MongoClient = require('mongodb').MongoClient;
|
const MongoClient = require('mongodb').MongoClient;
|
||||||
const faker = require('faker');
|
const faker = require('faker');
|
||||||
const shortId = require('shortid');
|
const shortid = require('shortid');
|
||||||
const slugg = require('slugg');
|
const slugg = require('slugg');
|
||||||
const { homeLocation } = require('../../../config/env.json');
|
const { homeLocation } = require('../../../config/env.json');
|
||||||
const debug = require('debug');
|
const debug = require('debug');
|
||||||
|
|
||||||
const log = debug('fcc:tools:seedNewsArticles');
|
const log = debug('fcc:tools:seedNewsArticles');
|
||||||
|
|
||||||
|
shortid.characters(
|
||||||
|
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$+'
|
||||||
|
);
|
||||||
|
|
||||||
|
const shortId = () => shortid.generate();
|
||||||
|
|
||||||
const { MONGOHQ_URL, NODE_ENV: env } = process.env;
|
const { MONGOHQ_URL, NODE_ENV: env } = process.env;
|
||||||
|
|
||||||
function handleError(err, client) {
|
function handleError(err, client) {
|
||||||
@ -61,7 +67,7 @@ const sixMonths = 15780000000;
|
|||||||
|
|
||||||
function generateArticle() {
|
function generateArticle() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const id = shortId.generate();
|
const id = shortId();
|
||||||
const title = faker.lorem.sentence();
|
const title = faker.lorem.sentence();
|
||||||
const paragraphs = faker.random.number(10) || 1;
|
const paragraphs = faker.random.number(10) || 1;
|
||||||
const arrayToLoopOver = new Array(paragraphs).fill('');
|
const arrayToLoopOver = new Array(paragraphs).fill('');
|
||||||
@ -79,7 +85,7 @@ function generateArticle() {
|
|||||||
username: faker.internet.userName()
|
username: faker.internet.userName()
|
||||||
},
|
},
|
||||||
featureImage: {
|
featureImage: {
|
||||||
src: faker.image.image(2000, 1300),
|
src: 'https://picsum.photos/2000/1300?random',
|
||||||
alt: faker.lorem.sentence(),
|
alt: faker.lorem.sentence(),
|
||||||
caption: paragraphs >= 5 ? faker.lorem.sentence() : ''
|
caption: paragraphs >= 5 ? faker.lorem.sentence() : ''
|
||||||
},
|
},
|
||||||
@ -92,7 +98,7 @@ function generateArticle() {
|
|||||||
() => `<p>${faker.lorem.paragraph()}</p>`
|
() => `<p>${faker.lorem.paragraph()}</p>`
|
||||||
),
|
),
|
||||||
published: true,
|
published: true,
|
||||||
featured: true,
|
featured: Math.random() < 0.6,
|
||||||
underReview: false,
|
underReview: false,
|
||||||
viewCount: faker.random.number(90000),
|
viewCount: faker.random.number(90000),
|
||||||
firstPublishedDate: fakeDate,
|
firstPublishedDate: fakeDate,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
require('dotenv').config();
|
const path = require('path');
|
||||||
|
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
|
||||||
|
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const kill = require('tree-kill');
|
const kill = require('tree-kill');
|
||||||
|
Reference in New Issue
Block a user