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 { faFacebookSquare, faRedditSquare, faTwitterSquare } from '@fortawesome/free-brands-svg-icons' |
||||
|
||||
import { getFacebookShareUrl, getRedditShareUrl, getTwitterShareUrl } from "lib/url"; |
||||
import { ShareIcon, ShareIconsList, ShareWrap } from './style'; |
||||
|
||||
const ShareGuide = (props) => ( |
||||
const ShareGuide = ({ |
||||
guide, |
||||
guide: { |
||||
author = {} |
||||
} = {} |
||||
}) => ( |
||||
<ShareWrap> |
||||
<ShareIconsList className="d-sm-none d-md-none d-lg-flex d-xl-flex"> |
||||
<ShareIcon><a href="#"><FontAwesomeIcon icon={ faTwitterSquare } /></a></ShareIcon> |
||||
<ShareIcon><a href="#"><FontAwesomeIcon icon={ faFacebookSquare } /></a></ShareIcon> |
||||
<ShareIcon><a href="#"><FontAwesomeIcon icon={ faRedditSquare } /></a></ShareIcon> |
||||
<ShareIcon> |
||||
<a href={ getTwitterShareUrl({ text: `${guide.title} by @${author.twitter}`, url: guide.url })} target="_blank"> |
||||
<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> |
||||
</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'; |
||||
|
||||
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 DefaultLayout from '../layouts/default'; |
||||
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 Index = () => ( |
||||
const Home = (props) => ( |
||||
<DefaultLayout> |
||||
<Home /> |
||||
<PageHeader /> |
||||
<HeroSection /> |
||||
<FeaturedContent /> |
||||
<PageFooter /> |
||||
</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