commit
c1e01f70cf
44 changed files with 2153 additions and 231 deletions
@ -0,0 +1,84 @@ |
|||||||
|
const path = require('path'); |
||||||
|
const fs = require('fs'); |
||||||
|
const guides = require('../data/guides'); |
||||||
|
const roadmaps = require('../data/roadmaps'); |
||||||
|
|
||||||
|
const { |
||||||
|
getPageRoutes, |
||||||
|
getGuideRoutes, |
||||||
|
getRoadmapRoutes |
||||||
|
} = require("../path-map"); |
||||||
|
|
||||||
|
describe("Build scripts tests", () => { |
||||||
|
test('it should generate valid pathmap for pages', () => { |
||||||
|
const pageRoutes = getPageRoutes(); |
||||||
|
|
||||||
|
expect(pageRoutes).toEqual({ |
||||||
|
'/': { page: '/index' }, |
||||||
|
'/about': { page: '/about' }, |
||||||
|
'/privacy': { page: '/privacy' }, |
||||||
|
'/terms': { page: '/terms' }, |
||||||
|
'/guides': { page: '/guides/index' }, |
||||||
|
'/roadmaps': { page: '/roadmaps' }, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
test('it should generate valid guides pathmap', () => { |
||||||
|
const expectedGuideRoutes = guides.reduce((acc, guide) => { |
||||||
|
const [,, slug] = guide.url.split('/'); |
||||||
|
return { |
||||||
|
...acc, |
||||||
|
[guide.url]: { |
||||||
|
page: '/guides/[guide]', |
||||||
|
query: slug, |
||||||
|
} |
||||||
|
}; |
||||||
|
}, {}); |
||||||
|
|
||||||
|
// Valid path map is generated
|
||||||
|
expect(expectedGuideRoutes).toEqual(getGuideRoutes()); |
||||||
|
|
||||||
|
const pageFilePath = path.join(__dirname, '../pages/guides/[guide].js'); |
||||||
|
const foundFilePath = fs.existsSync(pageFilePath) ? pageFilePath : ''; |
||||||
|
|
||||||
|
// Given page component exists
|
||||||
|
expect(foundFilePath).toEqual(pageFilePath); |
||||||
|
}); |
||||||
|
|
||||||
|
test('it should have markdown file for each guide', () => { |
||||||
|
guides.forEach(guide => { |
||||||
|
const [,, slug] = guide.url.split('/'); |
||||||
|
|
||||||
|
const expectedFile = path.join(__dirname, `../data/guides/${slug}.md`); |
||||||
|
const foundFile = fs.existsSync(expectedFile) ? expectedFile : ''; |
||||||
|
|
||||||
|
expect(foundFile).toEqual(expectedFile); |
||||||
|
}) |
||||||
|
}); |
||||||
|
|
||||||
|
test('it should generate valid roadmap routes', () => { |
||||||
|
const expectedPathMap = roadmaps.reduce((roadmapAcc, roadmap) => { |
||||||
|
// Routes for each of the versions of this roadmap
|
||||||
|
const versionRoutes = roadmap.versions.reduce((versionAcc, version) => ({ |
||||||
|
...versionAcc, |
||||||
|
[`${roadmap.url}/${version}`]: { |
||||||
|
page: '/[roadmap]/[version]', |
||||||
|
query: `${roadmap.url.split('/')[1]}/${version}`, |
||||||
|
} |
||||||
|
}), {}); |
||||||
|
|
||||||
|
// Route for the route roadmap itself
|
||||||
|
return { |
||||||
|
...roadmapAcc, |
||||||
|
[roadmap.url]: { |
||||||
|
page: '/[roadmap]/index', |
||||||
|
query: roadmap.url.split('/')[1] |
||||||
|
}, |
||||||
|
// Expected roadmap for versions
|
||||||
|
...versionRoutes |
||||||
|
}; |
||||||
|
}, {}); |
||||||
|
|
||||||
|
expect(getRoadmapRoutes()).toEqual(expectedPathMap); |
||||||
|
}) |
||||||
|
}); |
@ -1,16 +1,34 @@ |
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; |
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; |
||||||
import { faFacebookSquare, faRedditSquare, faTwitterSquare } from '@fortawesome/free-brands-svg-icons' |
import { faFacebookSquare, faRedditSquare, faTwitterSquare } from '@fortawesome/free-brands-svg-icons' |
||||||
|
|
||||||
|
import { getFacebookShareUrl, getRedditShareUrl, getTwitterShareUrl } from "lib/url"; |
||||||
import { ShareIcon, ShareIconsList, ShareWrap } from './style'; |
import { ShareIcon, ShareIconsList, ShareWrap } from './style'; |
||||||
|
|
||||||
const ShareGuide = (props) => ( |
const ShareGuide = ({ |
||||||
|
guide, |
||||||
|
guide: { |
||||||
|
author = {} |
||||||
|
} = {} |
||||||
|
}) => ( |
||||||
<ShareWrap> |
<ShareWrap> |
||||||
<ShareIconsList className="d-sm-none d-md-none d-lg-flex d-xl-flex"> |
<ShareIconsList className="d-sm-none d-md-none d-lg-flex d-xl-flex"> |
||||||
<ShareIcon><a href="#"><FontAwesomeIcon icon={ faTwitterSquare } /></a></ShareIcon> |
<ShareIcon> |
||||||
<ShareIcon><a href="#"><FontAwesomeIcon icon={ faFacebookSquare } /></a></ShareIcon> |
<a href={ getTwitterShareUrl({ text: `${guide.title} by @${author.twitter}`, url: guide.url })} target="_blank"> |
||||||
<ShareIcon><a href="#"><FontAwesomeIcon icon={ faRedditSquare } /></a></ShareIcon> |
<FontAwesomeIcon icon={ faTwitterSquare } /> |
||||||
|
</a> |
||||||
|
</ShareIcon> |
||||||
|
<ShareIcon> |
||||||
|
<a href={ getFacebookShareUrl({ text: guide.title, url: guide.url }) } target="_blank"> |
||||||
|
<FontAwesomeIcon icon={ faFacebookSquare } /> |
||||||
|
</a> |
||||||
|
</ShareIcon> |
||||||
|
<ShareIcon> |
||||||
|
<a href={ getRedditShareUrl({ text: guide.title, url: guide.url })} target="_blank"> |
||||||
|
<FontAwesomeIcon icon={ faRedditSquare } /> |
||||||
|
</a> |
||||||
|
</ShareIcon> |
||||||
</ShareIconsList> |
</ShareIconsList> |
||||||
</ShareWrap> |
</ShareWrap> |
||||||
); |
); |
||||||
|
|
||||||
export default ShareGuide; |
export default ShareGuide; |
||||||
|
@ -0,0 +1,6 @@ |
|||||||
|
{ |
||||||
|
"twitter": "roadmapsh", |
||||||
|
"url": "https://roadmap.sh", |
||||||
|
"repoUrl": "https://github.com/kamranahmedse/roadmap-next", |
||||||
|
"dataUrl": "/tree/master/data" |
||||||
|
} |
@ -1,3 +1,3 @@ |
|||||||
import authors from "../data/authors"; |
import authors from "data/authors"; |
||||||
|
|
||||||
export const findByUsername = (username) => authors.find(author => author.username === username) || {}; |
export const findByUsername = (username) => authors.find(author => author.username === username) || {}; |
||||||
|
@ -0,0 +1,32 @@ |
|||||||
|
import guides from "data/guides"; |
||||||
|
import authors from "data/authors"; |
||||||
|
import siteConfig from "data/site"; |
||||||
|
|
||||||
|
|
||||||
|
export const getRequestedGuide = req => { |
||||||
|
const guide = guides.find(guide => guide.url === req.url); |
||||||
|
if (!guide) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// We will use this URL format to find the relevant markdown
|
||||||
|
// file inside the `/data` directory. For example `/guides/learn-regex`
|
||||||
|
// has to have `/guides/learn-regex.md` file inside the `data` directory
|
||||||
|
const path = guide.url.replace(/^\//, ''); |
||||||
|
|
||||||
|
try { |
||||||
|
return { |
||||||
|
...guide, |
||||||
|
author: authors.find(author => author.username === guide.author) || {}, |
||||||
|
component: require(`../data/${path}.md`).default, |
||||||
|
}; |
||||||
|
} catch (e) { |
||||||
|
console.log(e); |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getContributionUrl = (guide) => { |
||||||
|
return `${siteConfig.repoUrl}${siteConfig.dataUrl}${guide.url}.md` |
||||||
|
}; |
@ -0,0 +1,50 @@ |
|||||||
|
import queryString from 'query-string'; |
||||||
|
import siteConfig from 'data/site'; |
||||||
|
|
||||||
|
export const prefixHost = (url) => { |
||||||
|
return /^\//.test(url) ? `${siteConfig.url}${url}` : url; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getTwitterUrl = (username) => { |
||||||
|
return `https://twitter.com/${username}`; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getTwitterShareUrl = ({ text, url }) => { |
||||||
|
const urlToShare = `${prefixHost(url)}?${queryString.stringify({ |
||||||
|
utm_source: 'roadmap.sh', |
||||||
|
utm_campaign: 'share', |
||||||
|
utm_medium: 'twitter', |
||||||
|
})}`;
|
||||||
|
|
||||||
|
return `https://twitter.com/intent/tweet?text=${text}&url=${encodeURI(urlToShare)}`; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getFacebookShareUrl = ({ text, url }) => { |
||||||
|
const urlToShare = `${prefixHost(url)}?${queryString.stringify({ |
||||||
|
utm_source: 'roadmap.sh', |
||||||
|
utm_campaign: 'share', |
||||||
|
utm_medium: 'facebook', |
||||||
|
})}`;
|
||||||
|
|
||||||
|
return `https://www.facebook.com/sharer/sharer.php?quote=${text}&u=${encodeURI(urlToShare)}`; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getRedditShareUrl = ({ text, url }) => { |
||||||
|
const urlToShare = `${prefixHost(url)}?${queryString.stringify({ |
||||||
|
utm_source: 'roadmap.sh', |
||||||
|
utm_campaign: 'share', |
||||||
|
utm_medium: 'reddit' |
||||||
|
})}`;
|
||||||
|
|
||||||
|
return `https://www.reddit.com/submit?title=${text}&url=${encodeURI(urlToShare)}`; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getHnShareUrl = ({ text, url }) => { |
||||||
|
const urlToShare = `${prefixHost(url)}?${queryString.stringify({ |
||||||
|
utm_source: 'roadmap.sh', |
||||||
|
utm_campaign: 'share', |
||||||
|
utm_medium: 'hn' |
||||||
|
})}`;
|
||||||
|
|
||||||
|
return `https://news.ycombinator.com/submitlink?t=${text}&u=${urlToShare}`; |
||||||
|
}; |
@ -1,3 +0,0 @@ |
|||||||
import OldRoadmap from './index'; |
|
||||||
|
|
||||||
export default OldRoadmap; |
|
@ -1,22 +0,0 @@ |
|||||||
import Error from 'next/error'; |
|
||||||
import Roadmap from '../roadmaps/[roadmap]/index'; |
|
||||||
import { serverOnlyProps } from '../../lib/server'; |
|
||||||
import { getRequestedRoadmap } from '../../lib/roadmap'; |
|
||||||
|
|
||||||
// Fallback page to handle the old roadmap pages implementation
|
|
||||||
const OldRoadmap = ({ roadmap }) => { |
|
||||||
if (roadmap) { |
|
||||||
return <Roadmap roadmap={ roadmap } /> |
|
||||||
} |
|
||||||
|
|
||||||
return <Error statusCode={ 404 } />; |
|
||||||
}; |
|
||||||
|
|
||||||
OldRoadmap.getInitialProps = serverOnlyProps(({ req }) => { |
|
||||||
return { |
|
||||||
roadmap: getRequestedRoadmap(req), |
|
||||||
}; |
|
||||||
}); |
|
||||||
|
|
||||||
|
|
||||||
export default OldRoadmap; |
|
@ -1,3 +1,3 @@ |
|||||||
import Roadmap from './index'; |
import Roadmap from './index'; |
||||||
|
|
||||||
export default Roadmap; |
export default Roadmap; |
@ -1,16 +0,0 @@ |
|||||||
import FeaturedContent from '../components/featured-content/index'; |
|
||||||
import HeroSection from '../components/hero-section/index'; |
|
||||||
import PageFooter from '../components/page-footer/index'; |
|
||||||
import PageHeader from '../components/page-header/index'; |
|
||||||
import DefaultLayout from '../layouts/default/index'; |
|
||||||
|
|
||||||
const Home = (props) => ( |
|
||||||
<DefaultLayout> |
|
||||||
<PageHeader /> |
|
||||||
<HeroSection /> |
|
||||||
<FeaturedContent /> |
|
||||||
<PageFooter /> |
|
||||||
</DefaultLayout> |
|
||||||
); |
|
||||||
|
|
||||||
export default Home; |
|
@ -1,10 +1,16 @@ |
|||||||
import Home from './home'; |
import FeaturedContent from 'components/featured-content/index'; |
||||||
import DefaultLayout from '../layouts/default'; |
import HeroSection from 'components/hero-section/index'; |
||||||
|
import PageFooter from 'components/page-footer/index'; |
||||||
|
import PageHeader from 'components/page-header/index'; |
||||||
|
import DefaultLayout from 'layouts/default/index'; |
||||||
|
|
||||||
const Index = () => ( |
const Home = (props) => ( |
||||||
<DefaultLayout> |
<DefaultLayout> |
||||||
<Home /> |
<PageHeader /> |
||||||
|
<HeroSection /> |
||||||
|
<FeaturedContent /> |
||||||
|
<PageFooter /> |
||||||
</DefaultLayout> |
</DefaultLayout> |
||||||
); |
); |
||||||
|
|
||||||
export default Index; |
export default Home; |
||||||
|
@ -0,0 +1,13 @@ |
|||||||
|
import DefaultLayout from 'layouts/default/index'; |
||||||
|
import PageHeader from 'components/page-header/index'; |
||||||
|
|
||||||
|
const RoadmapsList = () => ( |
||||||
|
<DefaultLayout> |
||||||
|
<PageHeader /> |
||||||
|
<div className="container"> |
||||||
|
<p>Show all roadmaps here</p> |
||||||
|
</div> |
||||||
|
</DefaultLayout> |
||||||
|
); |
||||||
|
|
||||||
|
export default RoadmapsList; |
@ -1,13 +0,0 @@ |
|||||||
import DefaultLayout from '../../layouts/default/index'; |
|
||||||
import PageHeader from '../../components/page-header/index'; |
|
||||||
|
|
||||||
const Roadmap = () => ( |
|
||||||
<DefaultLayout> |
|
||||||
<PageHeader /> |
|
||||||
<div className="container"> |
|
||||||
<p>Show all roadmaps here</p> |
|
||||||
</div> |
|
||||||
</DefaultLayout> |
|
||||||
); |
|
||||||
|
|
||||||
export default Roadmap; |
|
@ -0,0 +1,87 @@ |
|||||||
|
const path = require('path'); |
||||||
|
const glob = require('glob'); |
||||||
|
|
||||||
|
const guides = require('./data/guides.json'); |
||||||
|
const roadmaps = require('./data/roadmaps'); |
||||||
|
|
||||||
|
const PAGES_PATH = path.join(__dirname, 'pages'); |
||||||
|
|
||||||
|
/** |
||||||
|
* Generate the page routes from the page files inside `/pages` |
||||||
|
* directory. Gives the format understood by next |
||||||
|
* { |
||||||
|
* '/slug': { page: '/path/to-file' } |
||||||
|
* } |
||||||
|
*/ |
||||||
|
const getPageRoutes = () => { |
||||||
|
const files = glob.sync(`${PAGES_PATH}/**/*.js`, { |
||||||
|
ignore: [ |
||||||
|
'**/_*.js', // private non-page files e.g. _document.js
|
||||||
|
'**/[[]*[]].js', // Ignore dynamic pages i.e. `page/[something].js` files
|
||||||
|
'**/[[]*[]]/*.js', // Ignore files inside dynamic pages i.e. `[something]/abc.js`
|
||||||
|
] |
||||||
|
}); |
||||||
|
|
||||||
|
const pageRoutes = {}; |
||||||
|
files.forEach(file => { |
||||||
|
const pageName = file.replace(PAGES_PATH, '').replace('.js', ''); |
||||||
|
const pagePath = pageName.replace('/index', '') || '/'; |
||||||
|
|
||||||
|
pageRoutes[pagePath] = { page: `${pageName}` } |
||||||
|
}); |
||||||
|
|
||||||
|
return pageRoutes; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates routes for guide pages |
||||||
|
* @returns {*} |
||||||
|
*/ |
||||||
|
const getGuideRoutes = () => { |
||||||
|
return guides.reduce((acc, guide) => { |
||||||
|
const [, , slug] = guide.url.split('/'); |
||||||
|
return { |
||||||
|
...acc, |
||||||
|
[guide.url]: { |
||||||
|
page: '/guides/[guide]', |
||||||
|
query: slug, |
||||||
|
} |
||||||
|
}; |
||||||
|
}, {}); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates routes for each of the roadmap and it's respective versions |
||||||
|
* @returns {*} |
||||||
|
*/ |
||||||
|
const getRoadmapRoutes = () => { |
||||||
|
return roadmaps.reduce((roadmapRoutes, roadmap) => { |
||||||
|
const [, slug] = roadmap.url.split('/'); |
||||||
|
|
||||||
|
return { |
||||||
|
...roadmapRoutes, |
||||||
|
// Default roadmap path i.e. `{ '/frontend': { page: '/[roadmap]/index', query: 'frontend' }`
|
||||||
|
[roadmap.url]: { |
||||||
|
page: '/[roadmap]/index', |
||||||
|
query: slug |
||||||
|
}, |
||||||
|
// Route for each of the versions of this roadmap i.e.
|
||||||
|
// `{ '/frontend/2019': { page: '/[roadmap]/[version]', query: 'frontend/2019' } }`
|
||||||
|
...(roadmap.versions.reduce((versionRoutes, version) => { |
||||||
|
return { |
||||||
|
...versionRoutes, |
||||||
|
[`${roadmap.url}/${version}`]: { |
||||||
|
page: '/[roadmap]/[version]', |
||||||
|
query: `${slug}/${version}` |
||||||
|
} |
||||||
|
}; |
||||||
|
}, {})), |
||||||
|
}; |
||||||
|
}, {}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
getPageRoutes, |
||||||
|
getGuideRoutes, |
||||||
|
getRoadmapRoutes, |
||||||
|
}; |
Loading…
Reference in new issue