Feat: News in the client app (#34392)
This commit is contained in:
@@ -32,7 +32,8 @@ module.exports = {
|
||||
'/certification/*',
|
||||
'/unsubscribed/*',
|
||||
'/user/*',
|
||||
'/settings/*'
|
||||
'/settings/*',
|
||||
'/n/*'
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -45,6 +46,12 @@ module.exports = {
|
||||
curriculumPath: localeChallengesRootDir
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'fcc-source-news',
|
||||
options: {
|
||||
maximumStaticRenderCount: 100
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-source-filesystem',
|
||||
options: {
|
||||
|
@@ -8,8 +8,10 @@ const {
|
||||
createChallengePages,
|
||||
createBlockIntroPages,
|
||||
createSuperBlockIntroPages,
|
||||
createGuideArticlePages
|
||||
createGuideArticlePages,
|
||||
createNewsArticle
|
||||
} = require('./utils/gatsby');
|
||||
const { createArticleSlug } = require('./utils/news');
|
||||
|
||||
const createByIdentityMap = {
|
||||
guideMarkdown: createGuideArticlePages,
|
||||
@@ -35,6 +37,15 @@ 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 }) {
|
||||
@@ -86,6 +97,19 @@ exports.createPages = function createPages({ graphql, actions }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
allNewsArticleNode(
|
||||
sort: { fields: firstPublishedDate, order: DESC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
shortId
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`).then(result => {
|
||||
if (result.errors) {
|
||||
@@ -126,6 +150,11 @@ exports.createPages = function createPages({ graphql, actions }) {
|
||||
return null;
|
||||
});
|
||||
|
||||
// Create news article pages
|
||||
result.data.allNewsArticleNode.edges.forEach(
|
||||
createNewsArticle(createPage)
|
||||
);
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
@@ -161,7 +190,9 @@ exports.onCreateWebpackConfig = ({ stage, rules, plugins, actions }) => {
|
||||
HOME_PATH: JSON.stringify(
|
||||
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()
|
||||
]
|
||||
|
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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.0.2.tgz",
|
||||
"integrity": "sha512-WR3dlgqJP4QNrLC4iXN/5/2WaLQQ0VijOOkmflqFGVJ6wLEpbSjo7c0ZeGIdtY8Crk7xBBp87sM6+Mkerz7alw==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.0.tgz",
|
||||
"integrity": "sha512-j4ZwhaHmwsCb4DlDOIWnI5YyKDNMoNThsmwEpfHx6a1EpsGZ9qYLxP++LMlmBRjtGptGHFsGItJ768snllFWpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
@@ -1216,6 +1217,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.0.0.tgz",
|
||||
"integrity": "sha512-vdjoYLDptCgvtJs57ULshak3iJe4NW3sJ3g36xVDGff5AE8P30S6A093EIEPjdi2noGhfuNOEkbxt3J3awFW1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/samsam": "2.1.0"
|
||||
},
|
||||
@@ -1224,6 +1226,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.0.tgz",
|
||||
"integrity": "sha512-5x2kFgJYupaF1ns/RmharQ90lQkd2ELS8A9X0ymkAAdemYHGtI2KiUHG8nX2WU0T1qgnOU5YMqnBM2V7NUanNw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-from": "^2.1.1"
|
||||
}
|
||||
@@ -1231,12 +1234,10 @@
|
||||
}
|
||||
},
|
||||
"@sinonjs/samsam": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.1.tgz",
|
||||
"integrity": "sha512-7oX6PXMulvdN37h88dvlvRyu61GYZau40fL4wEZvPEHvrjpJc3lDv6xDM5n4Z0StufUVB5nDvVZUM+jZHdMOOQ==",
|
||||
"requires": {
|
||||
"array-from": "^2.1.1"
|
||||
}
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-2.1.2.tgz",
|
||||
"integrity": "sha512-ZwTHAlC9akprWDinwEPD4kOuwaYZlyMwVJIANsKNC3QVp0AHB04m7RnB4eqeWfgmxw8MGTzS9uMaw93Z3QcZbw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/configstore": {
|
||||
"version": "2.1.1",
|
||||
@@ -1781,7 +1782,8 @@
|
||||
"array-from": {
|
||||
"version": "2.1.1",
|
||||
"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": {
|
||||
"version": "3.0.3",
|
||||
@@ -4731,7 +4733,8 @@
|
||||
"diff": {
|
||||
"version": "3.5.0",
|
||||
"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": {
|
||||
"version": "5.0.3",
|
||||
@@ -10758,7 +10761,8 @@
|
||||
"just-extend": {
|
||||
"version": "3.0.0",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
@@ -11017,7 +11021,8 @@
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"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": {
|
||||
"version": "4.5.0",
|
||||
@@ -11143,7 +11148,8 @@
|
||||
"lolex": {
|
||||
"version": "2.7.5",
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
@@ -11779,9 +11785,10 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"nise": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-1.4.5.tgz",
|
||||
"integrity": "sha512-OHRVvdxKgwZELf2DTgsJEIA4MOq8XWvpSUzoOXyxJ2mY0mMENWC66+70AShLR2z05B1dzrzWlUQJmJERlOUpZw==",
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-1.4.6.tgz",
|
||||
"integrity": "sha512-1GedetLKzmqmgwabuMSqPsT7oumdR77SBpDfNNJhADRIeA3LN/2RVqR4fFqwvzhAqcTef6PPCzQwITE/YQ8S8A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/formatio": "3.0.0",
|
||||
"just-extend": "^3.0.0",
|
||||
@@ -11793,12 +11800,14 @@
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
|
||||
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"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": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
|
||||
@@ -15923,21 +15949,27 @@
|
||||
}
|
||||
},
|
||||
"sinon": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.4.tgz",
|
||||
"integrity": "sha512-NIaR56Z1mefuRBXYrf4otqBxkWiKveX+fvqs3HzFq2b07HcgpkMgIwmQM/owNjNFAHkx0kJXW+Q0mDthiuslXw==",
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-6.3.5.tgz",
|
||||
"integrity": "sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.0.2",
|
||||
"@sinonjs/formatio": "^3.0.0",
|
||||
"@sinonjs/samsam": "^2.1.1",
|
||||
"@sinonjs/samsam": "^2.1.2",
|
||||
"diff": "^3.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lolex": "^2.7.4",
|
||||
"lolex": "^2.7.5",
|
||||
"nise": "^1.4.5",
|
||||
"supports-color": "^5.5.0",
|
||||
"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": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz",
|
||||
@@ -17150,7 +17182,8 @@
|
||||
"text-encoding": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
|
||||
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk="
|
||||
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
|
||||
"dev": true
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
@@ -18953,6 +18986,26 @@
|
||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||
"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": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yurnalist/-/yurnalist-0.2.1.tgz",
|
||||
|
@@ -50,6 +50,7 @@
|
||||
"react-reflex": "^2.2.9",
|
||||
"react-spinkit": "^3.0.0",
|
||||
"react-stripe-elements": "^2.0.1",
|
||||
"react-youtube": "^7.8.0",
|
||||
"redux": "^4.0.0",
|
||||
"redux-actions": "^2.6.1",
|
||||
"redux-devtools-extension": "^2.13.5",
|
||||
@@ -58,7 +59,6 @@
|
||||
"redux-saga": "^0.16.0",
|
||||
"reselect": "^3.0.1",
|
||||
"rxjs": "^6.3.3",
|
||||
"sinon": "^6.3.4",
|
||||
"store": "^2.0.12",
|
||||
"validator": "^10.7.0",
|
||||
"webpack-remove-serviceworker-plugin": "^1.0.0"
|
||||
@@ -88,6 +88,7 @@
|
||||
"prettier": "^1.14.2",
|
||||
"prettier-eslint-cli": "^4.7.1",
|
||||
"react-test-renderer": "^16.5.2",
|
||||
"sinon": "^6.3.5",
|
||||
"webpack-cli": "^3.1.1"
|
||||
},
|
||||
"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,
|
||||
profileUI: PropTypes.object
|
||||
}),
|
||||
showLoading: PropTypes.bool,
|
||||
splat: PropTypes.string
|
||||
showLoading: PropTypes.bool
|
||||
};
|
||||
|
||||
const createRequestedUserSelector = () => (state, { maybeUser }) =>
|
||||
@@ -49,22 +48,15 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
class ShowFourOhFour extends Component {
|
||||
componentDidMount() {
|
||||
const { requestedUser, maybeUser, splat, fetchProfileForUser } = this.props;
|
||||
if (!splat && isEmpty(requestedUser)) {
|
||||
console.log(requestedUser);
|
||||
const { requestedUser, maybeUser, fetchProfileForUser } = this.props;
|
||||
if (isEmpty(requestedUser)) {
|
||||
return fetchProfileForUser(maybeUser);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSessionUser, requestedUser, showLoading, splat } = 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 />;
|
||||
}
|
||||
const { isSessionUser, requestedUser, showLoading } = this.props;
|
||||
if (showLoading) {
|
||||
// We don't know if /:maybeUser is a user or not, we will show the loader
|
||||
// until we get a response from the API
|
||||
|
@@ -3,7 +3,7 @@ import nanoId from 'nanoid';
|
||||
|
||||
import { createTypes } from '../../../utils/createTypes';
|
||||
|
||||
const ns = 'flash';
|
||||
export const ns = 'flash';
|
||||
|
||||
const initialState = {
|
||||
messages: []
|
||||
|
@@ -7,7 +7,6 @@ header {
|
||||
|
||||
#top-nav {
|
||||
background: #006400;
|
||||
margin-bottom: 0.45rem;
|
||||
height: 38px;
|
||||
margin-bottom: 0px;
|
||||
border-radius: 0;
|
||||
@@ -42,7 +41,8 @@ header {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#top-right-nav a, #top-right-nav img {
|
||||
#top-right-nav a,
|
||||
#top-right-nav img {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
@@ -62,15 +62,43 @@ header {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin: 0 3px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
#top-right-nav li > a, #top-right-nav li > span {
|
||||
color:#fff;
|
||||
#top-right-nav li,
|
||||
#top-right-nav li > a {
|
||||
color: #fff;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -92,4 +120,4 @@ header {
|
||||
}
|
||||
.ais-Hits {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,9 @@ function Header({ disableSettings }) {
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link to='/news'>News</Link>
|
||||
</li>
|
||||
<li className='user-state-link'>
|
||||
<UserState disableSettings={disableSettings} />
|
||||
</li>
|
||||
</ul>
|
||||
|
@@ -2,7 +2,7 @@ import { createAction, handleActions } from 'redux-actions';
|
||||
|
||||
import { createTypes } from '../../../../utils/stateManagement';
|
||||
|
||||
const ns = 'curriculumMap';
|
||||
export const ns = 'curriculumMap';
|
||||
|
||||
export const getNS = () => ns;
|
||||
|
||||
|
@@ -1,10 +1,3 @@
|
||||
import { navigate } from 'gatsby';
|
||||
import createRedirect from './createRedirect';
|
||||
|
||||
const RedirectHome = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
navigate('/');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RedirectHome;
|
||||
export default createRedirect('/');
|
||||
|
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 { 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 ShowDynamicNewsOrFourOhFour from '../client-only-routes/ShowDynamicNewsOrFourOhFour';
|
||||
/* eslint-enable max-len */
|
||||
|
||||
function FourOhFourPage() {
|
||||
return (
|
||||
<Router>
|
||||
<ShowProfileOrFourOhFour path='/:maybeUser/:splat' />
|
||||
<ShowProfileOrFourOhFour path='/:maybeUser' />
|
||||
<ShowDynamicNewsOrFourOhFour path='/news/:author/:articleSlug' />
|
||||
<RedirectNews path='/news/:author' />
|
||||
<NotFoundPage default={true} />
|
||||
</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 ns = 'app';
|
||||
export const ns = 'app';
|
||||
|
||||
const defaultFetchState = {
|
||||
export const defaultFetchState = {
|
||||
pending: true,
|
||||
complete: false,
|
||||
errored: false,
|
||||
@@ -220,16 +220,15 @@ export const reducer = handleActions(
|
||||
[username]: { ...previousUserObject, ...user }
|
||||
},
|
||||
userProfileFetchState: {
|
||||
...defaultFetchState,
|
||||
pending: false,
|
||||
complete: true,
|
||||
errored: false,
|
||||
error: null
|
||||
complete: true
|
||||
}
|
||||
};
|
||||
},
|
||||
[types.fetchProfileForUserError]: (state, { payload }) => ({
|
||||
...state,
|
||||
userFetchState: {
|
||||
userProfileFetchState: {
|
||||
pending: false,
|
||||
complete: false,
|
||||
errored: true,
|
||||
@@ -253,10 +252,9 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
showCert: payload,
|
||||
showCertFetchState: {
|
||||
...defaultFetchState,
|
||||
pending: false,
|
||||
complete: true,
|
||||
errored: false,
|
||||
error: null
|
||||
complete: true
|
||||
}
|
||||
}),
|
||||
[types.showCertError]: (state, { payload }) => ({
|
||||
|
@@ -1,17 +1,28 @@
|
||||
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 flash } from '../components/Flash/redux';
|
||||
import { reducer as settings } from './settings';
|
||||
import { reducer as curriculumMap } from '../components/Map/redux';
|
||||
import { reducer as challenge } from '../templates/Challenges/redux';
|
||||
import { reducer as app, ns as appNameSpace } from './';
|
||||
import {
|
||||
reducer as flash,
|
||||
ns as flashNameSpace
|
||||
} 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({
|
||||
app,
|
||||
challenge,
|
||||
curriculumMap,
|
||||
flash,
|
||||
[appNameSpace]: app,
|
||||
[challengeNameSpace]: challenge,
|
||||
[curriculumMapNameSpace]: curriculumMap,
|
||||
[flashNameSpace]: flash,
|
||||
form: formReducer,
|
||||
settings
|
||||
[newsNameSpace]: news,
|
||||
[settingsNameSpace]: settings
|
||||
});
|
||||
|
@@ -1,9 +1,17 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
|
||||
import errorSagas from './error-saga';
|
||||
import { sagas as appSagas } from './';
|
||||
import { sagas as settingsSagas } from './settings';
|
||||
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() {
|
||||
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 { createUpdateMyEmailSaga } from './update-email-saga';
|
||||
|
||||
const ns = 'settings';
|
||||
export const ns = 'settings';
|
||||
|
||||
const defaultFetchState = {
|
||||
pending: false,
|
||||
|
@@ -15,7 +15,7 @@ import currentChallengeEpic from './current-challenge-epic';
|
||||
|
||||
import { createIdToNameMapSaga } from './id-to-name-map-saga';
|
||||
|
||||
const ns = 'challenge';
|
||||
export const ns = 'challenge';
|
||||
export const backendNS = 'backendChallenge';
|
||||
|
||||
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;
|
||||
}
|
1
client/src/templates/News/Featured/index.js
Normal file
1
client/src/templates/News/Featured/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Featured';
|
62
client/src/templates/News/NewsApp.js
Normal file
62
client/src/templates/News/NewsApp.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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;
|
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);
|
60
client/src/templates/News/ShowArticle/components/Author.js
Normal file
60
client/src/templates/News/ShowArticle/components/Author.js
Normal file
@@ -0,0 +1,60 @@
|
||||
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 height='50px' src={avatar} />
|
||||
<div className='author-bio'>
|
||||
<ArticleMeta article={article} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Author.displayName = 'Author';
|
||||
Author.propTypes = propTypes;
|
||||
|
||||
export default Author;
|
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%;
|
||||
}
|
90
client/src/templates/News/components/ArticleMeta.js
Normal file
90
client/src/templates/News/components/ArticleMeta.js
Normal file
@@ -0,0 +1,90 @@
|
||||
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>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ArticleMeta.displayName = 'ArticleMeta';
|
||||
ArticleMeta.propTypes = propTypes;
|
||||
|
||||
export default ArticleMeta;
|
52
client/src/templates/News/components/BannerWide.js
Normal file
52
client/src/templates/News/components/BannerWide.js
Normal file
@@ -0,0 +1,52 @@
|
||||
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;
|
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 qs from 'query-string';
|
||||
|
||||
const base = '/internal';
|
||||
|
||||
@@ -40,8 +41,31 @@ export function getUsernameExists(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 **/
|
||||
|
||||
export function postPopularityEvent(event) {
|
||||
return post('/p', event);
|
||||
}
|
||||
|
||||
export function postReportUser(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
|
||||
// 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(/\/*$/, '');
|
||||
};
|
||||
|
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 select = require('unist-util-select');
|
||||
const { head } = require('lodash');
|
||||
const challengePageCreators = require('./challengePageCreator');
|
||||
const guidePageCreators = require('./guidePageCreator');
|
||||
const newsPageCreators = require('./newsPageCreator');
|
||||
|
||||
const { dasherize } = require('..');
|
||||
const { isAStubRE } = require('../regEx');
|
||||
|
||||
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 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
|
||||
}
|
||||
});
|
||||
module.exports = {
|
||||
...challengePageCreators,
|
||||
...guidePageCreators,
|
||||
...newsPageCreators
|
||||
};
|
||||
|
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);
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user