feat: remove news from platform

This commit is contained in:
Mrugesh Mohapatra
2019-01-14 22:49:13 +05:30
committed by Stuart Taylor
parent 219abdc2ce
commit fdc2219f81
39 changed files with 19 additions and 1709 deletions

View File

@ -1,34 +1,28 @@
import { has, pick, isEmpty } from 'lodash';
import debug from 'debug';
import { reportError } from '../middlewares/error-reporter';
const log = debug('fcc:boot:news');
export default function newsBoot(app) {
const api = app.loopback.Router();
const router = app.loopback.Router();
api.get('/n/:shortId', createShortLinkHandler(app));
api.post('/p', createPopularityHandler(app));
app.use('/internal', api);
router.get('/n', (req, res) => res.redirect('/news'));
router.get('/n/:shortId', createShortLinkHandler(app));
}
function createShortLinkHandler(app) {
const { Article } = app.models;
const referralHandler = createReferralHandler(app);
return function shortLinkHandler(req, res, next) {
const { query, user } = req;
const { query } = req;
const { shortId } = req.params;
// We manually report the error here as it should not affect this request
referralHandler(query, shortId, !!user).catch(err => reportError(err));
log(req.origin);
log(query.refsource);
if (!shortId) {
return res.sendStatus(400);
return res.redirect('/news');
}
log('shortId', shortId);
return Article.findOne(
{
where: {
@ -40,220 +34,14 @@ function createShortLinkHandler(app) {
next(err);
}
if (!article) {
return res.status(404).send('Could not find article by shortId');
return res.redirect('/news');
}
const {
slugPart,
shortId,
author: { username }
slugPart
} = 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);
const slug = `/news/${slugPart}`;
return res.redirect(slug);
}
);
};
}
function createPopularityHandler(app) {
const { Article, Popularity } = app.models;
function findArticleByShortId(shortId) {
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);
})
);
}
function findPopularityByShortId(shortId) {
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);
}
)
);
}
function createPopularity(popularityUpdate, shortId) {
return new Promise((resolve, reject) =>
Popularity.create(
{
events: [popularityUpdate],
articleId: shortId
},
err => {
if (err) {
return reject(err);
}
log('poulartiy created');
return resolve();
}
)
);
}
function updatePopularity(popularity, popularityUpdate) {
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();
}
)
);
}
function incrementArticleViews(article) {
return new Promise((resolve, reject) =>
article.updateAttributes({ $inc: { viewCount: 1 } }, err => {
if (err) {
log(err);
return reject(err);
}
log('article views updated');
return resolve();
})
);
}
return async function handlePopularityStats(req, res, next) {
const { body, user } = req;
if (
!has(body, 'event') ||
!has(body, 'timestamp') ||
!has(body, 'shortId')
) {
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;
log('shortId', shortId);
const articlePromise = findArticleByShortId(shortId);
const popularityPromise = findPopularityByShortId(shortId);
const [article, popularity] = await Promise.all([
articlePromise,
popularityPromise
]).catch(err => {
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();
}
);
}
)
);
};
}

View File

@ -12,10 +12,8 @@ const pathsOfNoReturn = [
];
const pathsWhiteList = [
'news',
'challenges',
'map',
'news',
'commit'
];

View File

@ -46,12 +46,6 @@ module.exports = {
curriculumPath: localeChallengesRootDir
}
},
{
resolve: 'fcc-source-news',
options: {
maximumStaticRenderCount: 100
}
},
{
resolve: 'gatsby-source-filesystem',
options: {

View File

@ -8,10 +8,8 @@ const {
createChallengePages,
createBlockIntroPages,
createSuperBlockIntroPages,
createGuideArticlePages,
createNewsArticle
createGuideArticlePages
} = require('./utils/gatsby');
const { createArticleSlug } = require('./utils/news');
const createByIdentityMap = {
guideMarkdown: createGuideArticlePages,
@ -37,15 +35,7 @@ exports.onCreateNode = function onCreateNode({ node, actions, getNode }) {
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 }) {
@ -97,19 +87,6 @@ exports.createPages = function createPages({ graphql, actions }) {
}
}
}
allNewsArticleNode(
sort: { fields: firstPublishedDate, order: DESC }
) {
edges {
node {
id
shortId
fields {
slug
}
}
}
}
}
`).then(result => {
if (result.errors) {
@ -150,11 +127,6 @@ exports.createPages = function createPages({ graphql, actions }) {
return null;
});
// Create news article pages
result.data.allNewsArticleNode.edges.forEach(
createNewsArticle(createPage)
);
return null;
})
);

View File

@ -1,26 +0,0 @@
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;

View File

@ -1,55 +0,0 @@
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();
});
}
)
);
};

View File

@ -1,80 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,6 +0,0 @@
{
"name": "fcc-source-news",
"dependencies": {
"mongodb": "^3.1.9"
}
}

View File

@ -1,16 +0,0 @@
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'
};

View File

@ -1,33 +0,0 @@
/* 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);
});
});
});

View File

@ -1,89 +0,0 @@
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);

View File

@ -22,7 +22,7 @@ function Header({ disableSettings }) {
</li>
<li>
<a
href='https://forum.freecodecamp.org'
href='https://www.freecodecamp.org/forum'
rel='noopener noreferrer'
target='_blank'
>

View File

@ -1,3 +0,0 @@
import createRedirect from './createRedirect';
export default createRedirect('/news');

View File

@ -2,18 +2,14 @@ import React from 'react';
import { Router } from '@reach/router';
import NotFoundPage from '../components/FourOhFour';
import RedirectNews from '../components/RedirectNews';
/* eslint-disable max-len */
import ShowProfileOrFourOhFour from '../client-only-routes/ShowProfileOrFourOhFour';
import ShowDynamicNewsOrFourOhFour from '../client-only-routes/ShowDynamicNewsOrFourOhFour';
/* eslint-enable max-len */
function FourOhFourPage() {
return (
<Router>
<ShowProfileOrFourOhFour path='/:maybeUser' />
<ShowDynamicNewsOrFourOhFour path='/news/:author/:articleSlug' />
<RedirectNews path='/news/:author' />
<NotFoundPage default={true} />
</Router>
);

View File

@ -1,15 +0,0 @@
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>
);
}

View File

@ -1,96 +0,0 @@
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;

View File

@ -15,7 +15,6 @@ 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({
[appNameSpace]: app,
@ -23,6 +22,5 @@ export default combineReducers({
[curriculumMapNameSpace]: curriculumMap,
[flashNameSpace]: flash,
form: formReducer,
[newsNameSpace]: news,
[settingsNameSpace]: settings
});

View File

@ -3,7 +3,6 @@ import { all } from 'redux-saga/effects';
import errorSagas from './error-saga';
import { sagas as appSagas } from './';
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() {
@ -11,7 +10,6 @@ export default function* rootSaga() {
...errorSagas,
...appSagas,
...challengeSagas,
...newsSagas,
...settingsSagas
]);
}

View File

@ -1,62 +0,0 @@
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;

View File

@ -1,33 +0,0 @@
.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;
}

View File

@ -1 +0,0 @@
export { default } from './Featured';

View File

@ -1,62 +0,0 @@
import React from 'react';
import { Grid } from 'react-bootstrap';
import Helmet from 'react-helmet';
import { SlimWidthRow } from '../common/app/helperComponents';
import Nav from './components/Nav';
import { routes } from './routes';
const propTypes = {};
/* eslint-disable max-len */
const styles = `
.app-layout p,
.app-layout li,
.app-layout a,
.app-layout span {
font-size: 21.5px;
}
.app-layout hr {
background-image: linear-gradient(to right, rgba(0, 100, 0, 0), rgba(0, 100, 0, 0.75), rgba(0, 100, 0, 0));
}
.app-layout p {
paddin-top: 8px;
}
.app-layout h1, .app-layout h2, .app-layout h3, .app-layout h4, .app-layout h5, .app-layout h6
{
padding-top: 35px;
padding-bottom: 0px;
}
.app-layout h1 {
font-size: 42px;
}
.app-layout h2 {
font-size: 34px;
}
.app-layout h3 {
font-size: 32px;
}
`;
/* eslint-enable max-len */
function NewsApp() {
return (
<div>
<Helmet>
<style>{styles}</style>
</Helmet>
<Nav />
<Grid fluid={true}>
<SlimWidthRow className='app-layout'>{routes}</SlimWidthRow>
</Grid>
</div>
);
}
NewsApp.displayName = 'NewsApp';
NewsApp.propTypes = propTypes;
export default NewsApp;

View File

@ -1,61 +0,0 @@
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);

View File

@ -1,60 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import ArticleMeta from '../../components/ArticleMeta';
const propTypes = {
article: PropTypes.shape({
author: PropTypes.objectOf(PropTypes.string)
})
};
const styles = `
.author-block {
display: flex;
align-items: center;
justify-content: center;
margin-top: 15px;
}
.author-block img {
border-radius: 20%;
}
.author-bio {
display: flex;
flex-direction: column;
margin-left: 30px;
}
.author-bio span {
font-size: 16px;
}
.author-block {
text-decoration: none;
}
`;
function Author({ article }) {
const {
author: { avatar }
} = article;
return (
<div className='author-block'>
<Helmet>
<style>{styles}</style>
</Helmet>
<img alt='' height='50px' src={avatar} />
<div className='author-bio'>
<ArticleMeta article={article} />
</div>
</div>
);
}
Author.displayName = 'Author';
Author.propTypes = propTypes;
export default Author;

View File

@ -1,187 +0,0 @@
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
}
}
`;

View File

@ -1,26 +0,0 @@
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
});

View File

@ -1,34 +0,0 @@
.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%;
}

View File

@ -1,90 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import differenceInMinutes from 'date-fns/difference_in_minutes';
import differenceInHours from 'date-fns/difference_in_hours';
import differenceInDays from 'date-fns/difference_in_calendar_days';
import format from 'date-fns/format';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { faCalendarAlt, faClock } from '@fortawesome/free-regular-svg-icons';
import { faFreeCodeCamp } from '@fortawesome/free-brands-svg-icons';
const propTypes = {
article: PropTypes.object
};
const styles = `
.meta-wrapper {
padding-top: 10px;
}
.meta-wrapper span,
.meta-wrapper a {
font-size: 16px;
}
.meta-item {
margin-right: 20px;
}
`;
function pluralise(singular, count) {
return `${singular}${count === 1 ? '' : 's'}`;
}
function getTimeString(pubDate) {
const now = new Date(Date.now());
const minuteDiff = differenceInMinutes(now, pubDate);
if (minuteDiff < 60) {
return `${minuteDiff} ${pluralise('minute', minuteDiff)} ago`;
}
const hourDiff = differenceInHours(now, pubDate);
if (hourDiff < 24) {
return `${hourDiff} ${pluralise('hour', hourDiff)} ago`;
}
const dayDiff = differenceInDays(now, pubDate);
if (dayDiff < 8) {
return `${dayDiff} ${pluralise('day', dayDiff)} ago`;
}
if (dayDiff < 365) {
return format(pubDate, 'MMM D');
}
return format(pubDate, 'MMM D YYYY');
}
function ArticleMeta({
article: { viewCount, author, meta, firstPublishedDate }
}) {
return (
<div className='meta-wrapper'>
<Helmet>
<style>{styles}</style>
</Helmet>
<div className='meta-item-wrapper'>
<span className='meta-item'>By {author.name}</span>
<span className='meta-item'>
<FontAwesomeIcon icon={faCalendarAlt} />{' '}
{getTimeString(firstPublishedDate)}
</span>
<span className='meta-item'>
<FontAwesomeIcon icon={faClock} /> {`${meta.readTime} minute read`}
</span>
{viewCount >= 100 && (
<span className='meta-item'>
<FontAwesomeIcon icon={faFreeCodeCamp} /> {`${viewCount} views`}
</span>
)}
</div>
</div>
);
}
ArticleMeta.displayName = 'ArticleMeta';
ArticleMeta.propTypes = propTypes;
export default ArticleMeta;

View File

@ -1,52 +0,0 @@
import React from 'react';
const propTypes = {};
function BannerWide() {
return (
<svg
version='1.0'
viewBox='0 0 1024 555'
xmlns='http://www.w3.org/2000/svg'
>
<path
d={
'M0 277.5V555h1024V0H0v277.5zm368.5-144.9c1.3.8 2.8 2.9 3.4 4.6 1.' +
'6 4.6-.6 8.3-14.2 22.6-14.3 15.2-22.8 26.8-30.1 41.1C315.4 225 31' +
'0 248 310 277c0 31.9 5.4 56.7 18 82.5 7.5 15.4 15.7 26.9 29.9 41.' +
'8 11.8 12.5 15.7 18.6 14.5 23.2-.8 3.4-5.9 7.5-9.3 7.5-8.3 0-21.5' +
'-11.3-36.7-31.6-19.9-26.4-30.5-51.1-36.6-85.1-2-11.2-2.3-16.1-2.2' +
'-38.3 0-23.9.1-26.3 2.6-38.1 6.6-30.6 18.8-55.7 39.6-81.6 17.6-21' +
'.7 30.5-30.1 38.7-24.7zm284 .3c6.4 2.9 14.5 10.2 23.6 21.1 28.7 3' +
'4.7 42.1 68.4 46 115.8 4.1 50.3-9.8 95.9-41.4 135.2-13.7 17.1-26.' +
'9 28-33.7 28-2.9 0-7.7-2.7-9-4.9-2.9-5.4-.7-9.2 13.7-24.3 16.8-17' +
'.6 26.7-31.9 34.4-49.7 19.4-45.2 17.8-104.8-4-149.6-8.5-17.4-16.8' +
'-28.8-34.3-47.2-4.9-5.1-9.3-10.4-9.8-11.8-2.2-5.7-.5-10.6 4.5-13.' +
'3 2.8-1.6 5.4-1.4 10 .7zm-172.9 16.5c16.2 4.3 31.4 16.9 38.3 31.7' +
' 3.8 8.2 4.8 11.8 9.1 34.4 2.5 12.8 4.8 18.7 8.4 21 4.5 3 11.4-.3' +
' 12.9-6.2.7-3.3-1-10-4.4-16.7-1.6-3.3-2.9-6.7-2.9-7.8 0-5.6 11.5 ' +
'2.1 23 15.3 16.1 18.7 21.7 36.1 20.8 64.4-.6 16.2-2.9 25-10 36.9-' +
'7.3 12.3-29.1 31.6-35.7 31.6-1.8 0-5.1-2.4-5.1-3.7 0-.3 2.8-3.5 6' +
'.1-7.1 12.2-13.3 16.8-22.2 17.6-34 .8-12.1-3.1-24.7-10-32-1.7-1.7' +
'-3.9-3.2-4.9-3.2-1.6 0-1.6.3-.4 3.8.7 2 1.7 5.6 2.1 8 .6 3.9.4 4.' +
'5-2.8 7.7-3.2 3.2-4 3.5-9.2 3.5-11.2 0-13-3.2-12.1-21 .6-11.8.5-1' +
'2.8-1.7-17.5-3.9-8.1-13.3-16.5-18.4-16.5-2.6 0-3.2 3.3-1 4.7 3.2 ' +
'2 4.7 5.8 4.7 12.3 0 10.1-3.2 15.5-15.2 26-15.3 13.4-18.8 19.4-18' +
'.8 32.4 0 17.4 8.4 32.8 20.8 38.3 3.9 1.7 5.2 2.8 5 4.1-.6 3-7 1.' +
'9-17.1-3-16.1-7.8-28-19-35.6-33.8-5.3-10.1-7.4-18.5-7.5-30-.1-15.' +
'9 3.8-25.3 24-57.6 17.8-28.6 22.1-39.2 21.2-53.1-.7-10.9-8.1-23.8' +
'-15.9-27.9-3-1.5-3.9-4.8-1.6-5.7 2.4-1 11.3-.6 16.3.7zm143.5 239.' +
'5c3.2 3.2 3.9 4.6 3.9 7.7 0 5.1-1.8 8.3-5.9 11.1l-3.4 2.3H512.8c-' +
'114.3 0-108 .3-112.7-5.6-1.4-1.8-2.1-4.1-2.1-7.2 0-3.8.5-5 3.9-8.' +
'3l3.9-3.9h213.4l3.9 3.9z'
}
fill='#006400'
/>
</svg>
);
}
BannerWide.displayName = 'BannerWide';
BannerWide.propTypes = propTypes;
export default BannerWide;

View File

@ -1,67 +0,0 @@
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
);

View File

@ -1,27 +0,0 @@
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)];
}

View File

@ -1,22 +1,6 @@
import { findIndex } from 'lodash';
// These regex are not for validation, it is purely to see
// if we are looking at something like what we want to validate
// before we try to validate
export const maybeEmailRE = /.*@.*\.\w\w/;
export const maybeUrlRE = /https?:\/\/.*\..*/;
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(/\/*$/, '');
};

View File

@ -1,45 +1,9 @@
/* 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);
describe('No tests for utils', () => {
it('No tests for utils', () => {
expect(true);
});
});
});

View File

@ -1,9 +1,7 @@
const challengePageCreators = require('./challengePageCreator');
const guidePageCreators = require('./guidePageCreator');
const newsPageCreators = require('./newsPageCreator');
module.exports = {
...challengePageCreators,
...guidePageCreators,
...newsPageCreators
...guidePageCreators
};

View File

@ -1,22 +0,0 @@
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
}
});

View File

@ -1,18 +0,0 @@
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)}`;
};

View File

@ -1,32 +0,0 @@
/* 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);
});
});
});

View File

@ -11,7 +11,6 @@
"lint": "echo 'Warning: TODO - Define Linting with fixing.'",
"seed": "npm-run-all -p seed:*",
"seed:challenges": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seedChallenges",
"seed:news": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seedNewsArticles",
"seed:auth-user": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seedAuthUser",
"start-develop": "node ./tools/scripts/start-develop.js",
"pretest": "npm-run-all -s test:lint",

View File

@ -1,126 +0,0 @@
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
const MongoClient = require('mongodb').MongoClient;
const faker = require('faker');
const shortid = require('shortid');
const slugg = require('slugg');
const { homeLocation } = require('../../../config/env.json');
const debug = require('debug');
const log = debug('fcc:tools:seedNewsArticles');
shortid.characters(
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$+'
);
const shortId = () => shortid.generate();
const { MONGOHQ_URL, NODE_ENV: env } = process.env;
function handleError(err, client) {
if (err) {
console.error('Oh noes!!');
console.error(err);
try {
client.close();
} catch (e) {
// no-op
} finally {
/* eslint-disable-next-line no-process-exit */
process.exit(1);
}
}
}
if (env !== 'production') {
MongoClient.connect(
MONGOHQ_URL,
{ useNewUrlParser: true },
async function(err, client) {
handleError(err, client);
log('Connected successfully to mongo');
const db = client.db('freecodecamp');
const articleCollection = db.collection('article');
const articles = stubArticles(200);
await articleCollection
.deleteMany({})
.catch(err => handleError(err, client));
return articleCollection
.insertMany(articles)
.then(({ insertedCount }) => {
log('inserted %d new articles', insertedCount);
client.close();
})
.catch(err => handleError(err, client));
}
);
}
function stubArticles(numberOfArticles = 1) {
return new Array(numberOfArticles).fill('').map(() => generateArticle());
}
const sixMonths = 15780000000;
function generateArticle() {
const now = Date.now();
const id = shortId();
const title = faker.lorem.sentence();
const paragraphs = faker.random.number(10) || 1;
const arrayToLoopOver = new Array(paragraphs).fill('');
const fakeDate = faker.date.between(new Date(now - sixMonths), new Date(now));
const fakeDateMs = new Date(fakeDate).getTime();
return {
shortId: id,
slugPart: slugg(title),
title,
author: {
name: faker.name.findName(),
avatar: faker.internet.avatar(),
twitter: 'https://twitter.com/camperbot',
bio: faker.lorem.sentence(),
username: faker.internet.userName()
},
featureImage: {
src: 'https://picsum.photos/2000/1300?random',
alt: faker.lorem.sentence(),
caption: paragraphs >= 5 ? faker.lorem.sentence() : ''
},
meta: {
readTime: paragraphs,
refLink: `${homeLocation}/n/${id}`
},
draft: 'this needs to be fixed',
renderableContent: arrayToLoopOver.map(
() => `<p>${faker.lorem.paragraph()}</p>`
),
published: true,
featured: Math.random() < 0.6,
underReview: false,
viewCount: faker.random.number(90000),
firstPublishedDate: fakeDate,
createdDate: fakeDate,
lastEditedDate: fakeDate,
history: [
{
event: 'created',
timestamp: fakeDateMs
},
{
event: 'edited',
timestamp: fakeDateMs
},
{
event: 'publish',
timestamp: fakeDateMs
},
{
event: 'featured',
timestamp: fakeDateMs
}
]
};
}