@ -1,18 +0,0 @@ |
|||||||
{ |
|
||||||
"extends": [ |
|
||||||
"next", |
|
||||||
"next/core-web-vitals", |
|
||||||
"prettier" |
|
||||||
], |
|
||||||
"rules": { |
|
||||||
"@next/next/no-img-element": [ |
|
||||||
"off" |
|
||||||
], |
|
||||||
"react/display-name": [ |
|
||||||
"off" |
|
||||||
], |
|
||||||
"react/jsx-no-target-blank": [ |
|
||||||
"off" |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
# These are supported funding model platforms |
|
||||||
github: kamranahmedse |
|
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
@ -1,33 +0,0 @@ |
|||||||
name: Deployment to GH Pages |
|
||||||
on: |
|
||||||
push: |
|
||||||
branches: [ master ] |
|
||||||
env: |
|
||||||
ROADMAP_GA_SECRET: ${{ secrets.GA_SECRET }} |
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
|
||||||
PAT: ${{ secrets.PAT }} |
|
||||||
CI: true |
|
||||||
NEXT_TELEMETRY_DISABLED: 1 |
|
||||||
jobs: |
|
||||||
build: |
|
||||||
runs-on: ubuntu-latest |
|
||||||
steps: |
|
||||||
- uses: actions/checkout@v2 |
|
||||||
with: |
|
||||||
persist-credentials: false |
|
||||||
- uses: actions/setup-node@v1 |
|
||||||
with: |
|
||||||
node-version: 16 |
|
||||||
- name: Setup Environment |
|
||||||
run: | |
|
||||||
npm install |
|
||||||
- name: Generate meta and build |
|
||||||
run: | |
|
||||||
npm run meta |
|
||||||
npm run build |
|
||||||
- name: Deploy to GH Pages |
|
||||||
run: | |
|
||||||
git config user.email "kamranahmed.se@gmail.com" |
|
||||||
git config user.name "Kamran Ahmed" |
|
||||||
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git |
|
||||||
npm run deploy |
|
@ -1,37 +0,0 @@ |
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
|
||||||
out |
|
||||||
|
|
||||||
# dependencies |
|
||||||
/node_modules |
|
||||||
/.pnp |
|
||||||
.pnp.js |
|
||||||
yarn.lock |
|
||||||
|
|
||||||
# testing |
|
||||||
/coverage |
|
||||||
|
|
||||||
# next.js |
|
||||||
/.next/ |
|
||||||
/out/ |
|
||||||
|
|
||||||
# production |
|
||||||
/build |
|
||||||
|
|
||||||
# misc |
|
||||||
.idea |
|
||||||
.DS_Store |
|
||||||
*.pem |
|
||||||
|
|
||||||
# debug |
|
||||||
npm-debug.log* |
|
||||||
yarn-debug.log* |
|
||||||
yarn-error.log* |
|
||||||
|
|
||||||
# local env files |
|
||||||
.env.local |
|
||||||
.env.development.local |
|
||||||
.env.test.local |
|
||||||
.env.production.local |
|
||||||
|
|
||||||
# vercel |
|
||||||
.vercel |
|
@ -1,5 +0,0 @@ |
|||||||
{ |
|
||||||
"semi": true, |
|
||||||
"singleQuote": true, |
|
||||||
"tabWidth": 2 |
|
||||||
} |
|
@ -1,76 +0,0 @@ |
|||||||
# Code of Conduct |
|
||||||
|
|
||||||
## Our Pledge |
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as |
|
||||||
contributors and maintainers pledge to make participation in our project and |
|
||||||
our community a harassment-free experience for everyone, regardless of age, body |
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression, |
|
||||||
level of experience, education, socio-economic status, nationality, personal |
|
||||||
appearance, race, religion, or sexual identity and orientation. |
|
||||||
|
|
||||||
## Our Standards |
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment |
|
||||||
include: |
|
||||||
|
|
||||||
* Using welcoming and inclusive language |
|
||||||
* Being respectful of differing viewpoints and experiences |
|
||||||
* Gracefully accepting constructive criticism |
|
||||||
* Focusing on what is best for the community |
|
||||||
* Showing empathy towards other community members |
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include: |
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or |
|
||||||
advances |
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks |
|
||||||
* Public or private harassment |
|
||||||
* Publishing others' private information, such as a physical or electronic |
|
||||||
address, without explicit permission |
|
||||||
* Other conduct which could reasonably be considered inappropriate in a |
|
||||||
professional setting |
|
||||||
|
|
||||||
## Our Responsibilities |
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable |
|
||||||
behavior and are expected to take appropriate and fair corrective action in |
|
||||||
response to any instances of unacceptable behavior. |
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or |
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions |
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or |
|
||||||
permanently any contributor for other behaviors that they deem inappropriate, |
|
||||||
threatening, offensive, or harmful. |
|
||||||
|
|
||||||
## Scope |
|
||||||
|
|
||||||
This Code of Conduct applies within all project spaces, and it also applies when |
|
||||||
an individual is representing the project or its community in public spaces. |
|
||||||
Examples of representing a project or community include using an official |
|
||||||
project e-mail address, posting via an official social media account, or acting |
|
||||||
as an appointed representative at an online or offline event. Representation of |
|
||||||
a project may be further defined and clarified by project maintainers. |
|
||||||
|
|
||||||
## Enforcement |
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
|
||||||
reported by contacting the project team at <kamranahmed.se@gmail.com>. All |
|
||||||
complaints will be reviewed and investigated and will result in a response that |
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is |
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident. |
|
||||||
Further details of specific enforcement policies may be posted separately. |
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good |
|
||||||
faith may face temporary or permanent repercussions as determined by other |
|
||||||
members of the project's leadership. |
|
||||||
|
|
||||||
## Attribution |
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org |
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see |
|
||||||
https://www.contributor-covenant.org/faq |
|
@ -1,60 +0,0 @@ |
|||||||
import { Box, Container, Flex, Heading, Image, Link, Text } from '@chakra-ui/react'; |
|
||||||
import React from 'react'; |
|
||||||
|
|
||||||
type ContentPageHeaderProps = { |
|
||||||
formattedDate: string; |
|
||||||
title: string; |
|
||||||
subtitle: string; |
|
||||||
author?: { |
|
||||||
name: string; |
|
||||||
twitter: string; |
|
||||||
picture: string; |
|
||||||
}, |
|
||||||
subLink?: { |
|
||||||
text: string; |
|
||||||
url: string; |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
export function ContentPageHeader(props: ContentPageHeaderProps) { |
|
||||||
const { title, subtitle, author = null, formattedDate, subLink = null } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box pt={['35px', '35px', '70px']} pb={['35px', '35px', '55px']} borderBottomWidth={1} mb='30px'> |
|
||||||
<Container maxW='container.md' position='relative' textAlign={['left', 'left', 'center']}> |
|
||||||
<Flex alignItems='center' justifyContent={['flex-start', 'flex-start', 'center']} |
|
||||||
fontSize={['12px', '12px', '14px']}> |
|
||||||
|
|
||||||
{author?.name && ( |
|
||||||
<> |
|
||||||
<Link |
|
||||||
d={['none', 'flex', 'flex']} |
|
||||||
target='_blank' |
|
||||||
href={`https://twitter.com/${author.twitter}`} |
|
||||||
alignItems='center' |
|
||||||
fontWeight={600} |
|
||||||
color='gray.500' |
|
||||||
> |
|
||||||
<Image alt={''} rounded={'full'} mr='7px' w='22px' src={author.picture} /> |
|
||||||
{author.name} |
|
||||||
</Link> |
|
||||||
<Text d={['none', 'inline', 'inline']} mx='7px' color='gray.500' as='span'>·</Text> |
|
||||||
</> |
|
||||||
)} |
|
||||||
|
|
||||||
<Text color='gray.500' as='span'>{formattedDate}</Text> |
|
||||||
{subLink?.text && ( |
|
||||||
<> |
|
||||||
<Text d={['none', 'none', 'inline']} mx='7px' color='gray.500' as='span'>·</Text> |
|
||||||
<Link d={['none', 'none', 'inline']} color='blue.500' fontWeight={500} |
|
||||||
href={subLink.url} target={'_blank'}>{subLink.text}</Link> |
|
||||||
</> |
|
||||||
)} |
|
||||||
</Flex> |
|
||||||
<Heading as='h1' color='black' fontSize={['30px', '30px', '45px']} lineHeight={['40px', '40px', '53px']} |
|
||||||
fontWeight={700} my={['5px', '5px', '10px']}>{title}</Heading> |
|
||||||
<Text fontSize={['14px', '14px', '16px']} color='gray.700'>{subtitle}</Text> |
|
||||||
</Container> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,69 +0,0 @@ |
|||||||
import { Box, Flex, Heading, Image, Link } from '@chakra-ui/react'; |
|
||||||
import { event } from '../lib/gtag'; |
|
||||||
|
|
||||||
function getPageSlug() { |
|
||||||
const pathname = (typeof window !== 'undefined' ? window : {} as any)?.location?.pathname || ''; |
|
||||||
|
|
||||||
return pathname?.replace(/\//g, ''); |
|
||||||
} |
|
||||||
|
|
||||||
export const CustomAd = () => { |
|
||||||
const slug = getPageSlug(); |
|
||||||
|
|
||||||
if (slug !== 'devops') { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<Link |
|
||||||
href='https://www.getambassador.io/edge-stack-guide-v4?utm_source=roadmap.sh&utm_medium=ebook&utm_campaign=edgestack-guide' |
|
||||||
id='custom-ad' |
|
||||||
pos='fixed' |
|
||||||
bottom='15px' |
|
||||||
right='20px' |
|
||||||
zIndex={999} |
|
||||||
display='flex' |
|
||||||
maxWidth='330px' |
|
||||||
bg='white' |
|
||||||
boxShadow='0 1px 4px 1px hsla(0, 0%, 0%, .1)' |
|
||||||
_hover={{ textDecoration: 'none' }} |
|
||||||
rel="noopener sponsored" |
|
||||||
target={'_blank'} |
|
||||||
onClick={() => { |
|
||||||
event({ |
|
||||||
category: 'SponsorClick', |
|
||||||
action: `Ambassador EBook Redirect`, |
|
||||||
label: `Clicked Ambassador EBook Link` |
|
||||||
}); |
|
||||||
}} |
|
||||||
> |
|
||||||
<Image |
|
||||||
src='https://i.imgur.com/0bH1Vl6.png' |
|
||||||
alt='Custom Logo' |
|
||||||
height={['100px', '100px', '100px', 'auto']} |
|
||||||
width='130' |
|
||||||
style={{ maxWidth: '130px', border: 'none' }} |
|
||||||
/> |
|
||||||
<Flex as='span' flexDirection='column' justifyContent='space-between'> |
|
||||||
<Box as='span' p='10px'> |
|
||||||
<Heading as='span' fontSize='14px' mb='5px' display='block'>Free eBook</Heading> |
|
||||||
<Box display='block' as='span' fontSize='13px' lineHeight={1.5} fontWeight={500} color='gray.500'> |
|
||||||
Learn about API Gateways, Microservices, Load Balancing, and more with this free eBook. |
|
||||||
</Box> |
|
||||||
</Box> |
|
||||||
<Box as='span' |
|
||||||
textAlign='center' |
|
||||||
fontWeight={600} |
|
||||||
fontSize='9px' |
|
||||||
letterSpacing='0.5px' |
|
||||||
textTransform='uppercase' |
|
||||||
padding='5px 10px' |
|
||||||
display={'block'} |
|
||||||
background='repeating-linear-gradient(-45deg, transparent, transparent 5px, hsla(0, 0%, 0%, .025) 5px, hsla(0, 0%, 0%, .025) 10px) hsla(203, 11%, 95%, .4)' |
|
||||||
> |
|
||||||
Partner Content |
|
||||||
</Box> |
|
||||||
</Flex> |
|
||||||
</Link> |
|
||||||
); |
|
||||||
}; |
|
@ -1,46 +0,0 @@ |
|||||||
import { Box, Link, Text } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type DimmedMoreProps = { |
|
||||||
text: string; |
|
||||||
href: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export function DimmedMore(props: DimmedMoreProps) { |
|
||||||
const { text, href } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box position='relative' textAlign='center' bottom='20px'> |
|
||||||
<Box |
|
||||||
opacity={1} |
|
||||||
pointerEvents='none' |
|
||||||
position='absolute' |
|
||||||
bottom={0} |
|
||||||
height='200px' |
|
||||||
width='100%' |
|
||||||
background='linear-gradient(180deg, rgb(255 255 255 / 40%), white)' |
|
||||||
/> |
|
||||||
|
|
||||||
<Link |
|
||||||
rounded='20px' |
|
||||||
display='inline' |
|
||||||
bg='green.600' |
|
||||||
color='white' |
|
||||||
p='7px 20px' |
|
||||||
href={href} |
|
||||||
fontWeight={800} |
|
||||||
fontSize='11px' |
|
||||||
textTransform='uppercase' |
|
||||||
my='25px' |
|
||||||
position='relative' |
|
||||||
_hover={{ |
|
||||||
textDecoration: 'none', |
|
||||||
'& .forward-arrow': { |
|
||||||
transform: 'translateX(3px)' |
|
||||||
} |
|
||||||
}}> |
|
||||||
{text} |
|
||||||
<Text d='inline-block' as='span' transition='200ms' ml='4px' className='forward-arrow'>→</Text> |
|
||||||
</Link> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,124 +0,0 @@ |
|||||||
import { Box, Container, Flex, Image, Link, SimpleGrid, Stack, Text } from '@chakra-ui/react'; |
|
||||||
import siteConfig from '../content/site.json'; |
|
||||||
import { CustomAd } from './custom-ad'; |
|
||||||
import React from 'react'; |
|
||||||
import { event } from '../lib/gtag'; |
|
||||||
|
|
||||||
function NavigationLinks() { |
|
||||||
return ( |
|
||||||
<> |
|
||||||
<Stack isInline display={['none', 'none', 'flex']} justifyContent='center' color='gray.400' fontWeight={600} |
|
||||||
spacing='30px'> |
|
||||||
<Link _hover={{ color: 'white' }} href='/roadmaps'>Roadmaps</Link> |
|
||||||
<Link _hover={{ color: 'white' }} href='/guides'>Guides</Link> |
|
||||||
<Link _hover={{ color: 'white' }} href='/watch'>Videos</Link> |
|
||||||
<Link _hover={{ color: 'white' }} href='/about'>About</Link> |
|
||||||
<Link _hover={{ color: 'white' }} href={siteConfig.url.youtube} target='_blank'>YouTube</Link> |
|
||||||
</Stack> |
|
||||||
|
|
||||||
<Stack display={['flex', 'flex', 'none']} color='gray.400' fontWeight={600} spacing={0}> |
|
||||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }} |
|
||||||
href='/roadmaps'>Roadmaps</Link> |
|
||||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }} |
|
||||||
href='/guides'>Guides</Link> |
|
||||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }} |
|
||||||
href='/watch'>Videos</Link> |
|
||||||
<Link py='7px' borderBottomWidth={1} borderBottomColor='gray.800' _hover={{ color: 'white' }} |
|
||||||
href='/about'>About</Link> |
|
||||||
<Link py='7px' _hover={{ color: 'white' }} target='_blank' |
|
||||||
href={siteConfig.url.youtube}>YouTube</Link> |
|
||||||
</Stack> |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function Footer() { |
|
||||||
return ( |
|
||||||
<Box bg='brand.hero' p={['25px 0', '25px 0', '40px 0']}> |
|
||||||
<Container maxW='container.md'> |
|
||||||
<NavigationLinks /> |
|
||||||
|
|
||||||
<SimpleGrid mt={['40px', '40px', '50px']} mb='40px' gap={['40px', '40px', '75px']} columns={[1, 1, 2, 2]} |
|
||||||
justifyContent='space-between'> |
|
||||||
<Box maxWidth={'550px'}> |
|
||||||
<Flex gap={0} alignItems='center' color='gray.400'> |
|
||||||
<Link d='flex' alignItems='center' fontWeight={600} _hover={{ textDecoration: 'none', color: 'white' }} |
|
||||||
href='/'> |
|
||||||
<Image alt='' h='25px' w='25px' src='/logo.svg' mr='6px' /> |
|
||||||
roadmap.sh |
|
||||||
</Link> |
|
||||||
<Text as='span' mx='7px'>by</Text> |
|
||||||
<Link bg='blue.500' px='6px' py='2px' rounded='4px' color='white' fontWeight={600} fontSize='13px' |
|
||||||
_hover={{ textDecoration: 'none', bg: 'blue.600' }} href={siteConfig.url.twitter} |
|
||||||
target='_blank'>@kamranahmedse</Link> |
|
||||||
</Flex> |
|
||||||
|
|
||||||
<Text my='15px' fontSize='14px' color='gray.500'>Community created roadmaps, articles, resources and |
|
||||||
journeys to help you choose your path and grow in your career.</Text> |
|
||||||
|
|
||||||
<Text fontSize='14px' color='gray.500'> |
|
||||||
<Text as='span' mr='10px'>© roadmap.sh</Text>· |
|
||||||
<Link href='/about' _hover={{ textDecoration: 'none', color: 'white' }} color='gray.400' |
|
||||||
mx='10px'>FAQs</Link>· |
|
||||||
<Link href='/terms' _hover={{ textDecoration: 'none', color: 'white' }} color='gray.400' |
|
||||||
mx='10px'>Terms</Link>· |
|
||||||
<Link href='/privacy' _hover={{ textDecoration: 'none', color: 'white' }} color='gray.400' |
|
||||||
mx='10px'>Privacy</Link> |
|
||||||
</Text> |
|
||||||
</Box> |
|
||||||
|
|
||||||
<Box maxWidth={'550px'} textAlign={['left', 'left', 'right']}> |
|
||||||
<Link display='flex' justifyContent={['flex-start', 'flex-start', 'flex-end']} fontWeight={600} |
|
||||||
_hover={{ textDecoration: 'none', color: 'white' }} |
|
||||||
href='https://thenewstack.io?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer' |
|
||||||
target='_blank'> |
|
||||||
<Image alt='' w='195px' src='/tns.png' /> |
|
||||||
</Link> |
|
||||||
|
|
||||||
<Text my='15px' fontSize='14px' color='gray.500'>The leading DevOps resource for Kubernetes, cloud-native |
|
||||||
computing, and the latest in at-scale development, deployment, and management.</Text> |
|
||||||
|
|
||||||
<Text fontSize='14px' color='gray.500'> |
|
||||||
<Link |
|
||||||
href='https://thenewstack.io/category/devops/?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer' |
|
||||||
target='_blank' |
|
||||||
_hover={{ textDecoration: 'none', color: 'white' }} |
|
||||||
onClick={() => { |
|
||||||
event({ |
|
||||||
category: 'PartnerClick', |
|
||||||
action: `TNS Referral`, |
|
||||||
label: `TNS Referral - Footer`, |
|
||||||
}); |
|
||||||
}} |
|
||||||
color='gray.400' mx='10px' ml={['0', '0', '10px']}>DevOps</Link>· |
|
||||||
<Link |
|
||||||
href='https://thenewstack.io/category/kubernetes/?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer' |
|
||||||
target='_blank' _hover={{ textDecoration: 'none', color: 'white' }} |
|
||||||
onClick={() => { |
|
||||||
event({ |
|
||||||
category: 'PartnerClick', |
|
||||||
action: `TNS Referral`, |
|
||||||
label: `TNS Referral - Footer`, |
|
||||||
}); |
|
||||||
}} |
|
||||||
color='gray.400' mx='10px'>Kubernetes</Link>· |
|
||||||
<Link |
|
||||||
href='https://thenewstack.io/category/cloud-native/?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Footer' |
|
||||||
target='_blank' _hover={{ textDecoration: 'none', color: 'white' }} |
|
||||||
onClick={() => { |
|
||||||
event({ |
|
||||||
category: 'PartnerClick', |
|
||||||
action: `TNS Referral`, |
|
||||||
label: `TNS Referral - Footer`, |
|
||||||
}); |
|
||||||
}} |
|
||||||
color='gray.400' mx='10px'>Cloud-Native</Link> |
|
||||||
</Text> |
|
||||||
</Box> |
|
||||||
</SimpleGrid> |
|
||||||
</Container> |
|
||||||
|
|
||||||
<CustomAd /> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,134 +0,0 @@ |
|||||||
import { useState } from 'react'; |
|
||||||
import { HamburgerIcon } from '@chakra-ui/icons'; |
|
||||||
import { Box, CloseButton, Container, Flex, IconButton, Link, Stack, Text } from '@chakra-ui/react'; |
|
||||||
import RoadmapLogo from '../components/icons/roadmap.svg'; |
|
||||||
|
|
||||||
type MenuLinkProps = { |
|
||||||
text: string; |
|
||||||
link: string; |
|
||||||
target?: '_blank' | '_self' | '_parent' | '_top'; |
|
||||||
isFancy?: boolean; |
|
||||||
}; |
|
||||||
|
|
||||||
function MenuLink(props: MenuLinkProps) { |
|
||||||
const { text, link, target = '_self', isFancy = false } = props; |
|
||||||
|
|
||||||
const gradientProp = isFancy ? { |
|
||||||
bgGradient: 'linear(to-r, yellow.100, teal.100)', |
|
||||||
bgClip: 'text', |
|
||||||
_hover: { |
|
||||||
color: 'yellow.100' |
|
||||||
} |
|
||||||
} : {}; |
|
||||||
|
|
||||||
return <Link |
|
||||||
borderBottomWidth={0} |
|
||||||
borderBottomColor='gray.500' |
|
||||||
_hover={{ textDecoration: 'none', borderBottomColor: 'white' }} |
|
||||||
fontWeight={500} |
|
||||||
href={link} |
|
||||||
target={target} |
|
||||||
{...gradientProp} |
|
||||||
> |
|
||||||
{text} |
|
||||||
</Link>; |
|
||||||
} |
|
||||||
|
|
||||||
function DesktopMenuLinks() { |
|
||||||
return ( |
|
||||||
<Stack d={['none', 'flex', 'flex']} shouldWrapChildren isInline spacing='15px' alignItems='center' color='gray.50' |
|
||||||
fontSize='15px'> |
|
||||||
<MenuLink text={'Roadmaps'} link={'/roadmaps'} /> |
|
||||||
<MenuLink text={'Guides'} link={'/guides'} /> |
|
||||||
|
|
||||||
<MenuLink |
|
||||||
target={'_blank'} |
|
||||||
text={'Hiring a DevRel'} |
|
||||||
isFancy |
|
||||||
link={'https://docs.google.com/forms/d/e/1FAIpQLSesFpPxgKx_8-L5hm7fw6NQpgGixrMGC4Cg3M8NHPQhFfSajQ/viewform'} |
|
||||||
/> |
|
||||||
|
|
||||||
<Link ml='10px' bgGradient='linear(to-l, yellow.700, red.600)' p='7px 10px' rounded='4px' |
|
||||||
_hover={{ textDecoration: 'none', bgGradient: 'linear(to-l, red.800, yellow.700)' }} |
|
||||||
fontWeight={500} href={'/signup'}>Subscribe</Link> |
|
||||||
</Stack> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function MobileMenuLinks() { |
|
||||||
const [isOpen, setIsOpen] = useState(false); |
|
||||||
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<IconButton |
|
||||||
rounded='5px' |
|
||||||
padding={0} |
|
||||||
aria-label={'Menu'} |
|
||||||
d={['block', 'none', 'none']} |
|
||||||
icon={<HamburgerIcon color='white' w='25px' height='25px' />} |
|
||||||
color='white' |
|
||||||
cursor='pointer' |
|
||||||
h='auto' |
|
||||||
bg='transparent' |
|
||||||
_hover={{ bg: 'transparent' }} |
|
||||||
_active={{ bg: 'transparent' }} |
|
||||||
_focus={{ bg: 'transparent' }} |
|
||||||
onClick={() => setIsOpen(true)} |
|
||||||
/> |
|
||||||
|
|
||||||
{isOpen && ( |
|
||||||
<Stack color='gray.100' |
|
||||||
fontSize={['22px', '22px', '22px', '32px']} |
|
||||||
alignItems='center' |
|
||||||
justifyContent='center' |
|
||||||
pos='fixed' |
|
||||||
left={0} |
|
||||||
right={0} |
|
||||||
bottom={0} |
|
||||||
top={0} |
|
||||||
bg='gray.900' |
|
||||||
spacing='12px' |
|
||||||
zIndex={999} |
|
||||||
> |
|
||||||
<Link href='/roadmaps'>Roadmaps</Link> |
|
||||||
<Link href='/guides'>Guides</Link> |
|
||||||
<Link href='/watch'>Videos</Link> |
|
||||||
<Link href='/signup'>Subscribe</Link> |
|
||||||
<CloseButton onClick={() => setIsOpen(false)} pos='fixed' top='40px' right='15px' size='lg' /> |
|
||||||
</Stack> |
|
||||||
)} |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
type GlobalHeaderProps = { |
|
||||||
variant?: 'transparent' | 'solid' |
|
||||||
}; |
|
||||||
|
|
||||||
export function GlobalHeader(props: GlobalHeaderProps) { |
|
||||||
const { variant = 'solid' } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box bg={variant === 'solid' ? 'gray.900' : 'transparent'} p='20px 0'> |
|
||||||
<Container maxW='container.md'> |
|
||||||
<Flex justifyContent='space-between' alignItems='center'> |
|
||||||
<Box> |
|
||||||
<Link w='100%' |
|
||||||
d='flex' |
|
||||||
href='/' |
|
||||||
alignItems='center' |
|
||||||
color='white' |
|
||||||
fontWeight={600} |
|
||||||
_hover={{ textDecoration: 'none' }} |
|
||||||
fontSize='18px'> |
|
||||||
<RoadmapLogo style={{ height: '30px', width: '30px', marginRight: '10px' }} /> |
|
||||||
<Text d={['block', 'none', 'block']} as='span'>roadmap.sh</Text> |
|
||||||
</Link> |
|
||||||
</Box> |
|
||||||
<DesktopMenuLinks /> |
|
||||||
<MobileMenuLinks /> |
|
||||||
</Flex> |
|
||||||
</Container> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,33 +0,0 @@ |
|||||||
import { Badge, Box, Heading, Link, Text } from '@chakra-ui/react'; |
|
||||||
import { GuideType } from '../../lib/guide'; |
|
||||||
|
|
||||||
type GuideGridItemProps = { |
|
||||||
title: string; |
|
||||||
href: string; |
|
||||||
subtitle: string; |
|
||||||
date: string; |
|
||||||
isNew?: boolean; |
|
||||||
colorIndex?: number; |
|
||||||
type?: GuideType['type']; |
|
||||||
}; |
|
||||||
|
|
||||||
const bgColorList = [ |
|
||||||
'gray.700', |
|
||||||
'purple.800' |
|
||||||
]; |
|
||||||
|
|
||||||
export function GuideGridItem(props: GuideGridItemProps) { |
|
||||||
const { title, subtitle, date, isNew = false, colorIndex = 0, href, type } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box _hover={{ textDecoration: 'none', transform: 'scale(1.02)' }} as={Link} href={href} shadow='xl' p='20px' |
|
||||||
rounded='10px' bg={bgColorList[colorIndex] ?? bgColorList[0]} flex={1}> |
|
||||||
<Text mb='10px' fontSize='13px' color='gray.400' textTransform='capitalize'> |
|
||||||
{isNew && <Badge colorScheme={'green'} mr='10px'>New</Badge>} |
|
||||||
{type} Guide |
|
||||||
</Text> |
|
||||||
<Heading color='white' mb={'6px'} fontSize='20px'>{title}</Heading> |
|
||||||
<Text color='gray.300' fontSize='14px'>{subtitle}</Text> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,150 +0,0 @@ |
|||||||
import NextHead from 'next/head'; |
|
||||||
import siteConfig from '../content/site.json'; |
|
||||||
import { RoadmapType } from '../lib/roadmap'; |
|
||||||
|
|
||||||
type HelmetProps = { |
|
||||||
title?: string; |
|
||||||
keywords?: string[]; |
|
||||||
canonical?: string; |
|
||||||
description?: string; |
|
||||||
noIndex?: boolean; |
|
||||||
roadmap?: RoadmapType; |
|
||||||
}; |
|
||||||
|
|
||||||
function getRichSnippetJson(roadmap: RoadmapType) { |
|
||||||
return { |
|
||||||
'@context': 'https://schema.org', |
|
||||||
'@type': 'Article', |
|
||||||
mainEntityOfPage: { |
|
||||||
'@type': 'WebPage', |
|
||||||
'@id': `https://roadmap.sh/${roadmap.id}`, |
|
||||||
}, |
|
||||||
headline: roadmap.seo.title, |
|
||||||
description: roadmap.seo.description, |
|
||||||
image: roadmap.jsonUrl |
|
||||||
? `https://roadmap.sh/roadmaps/${roadmap.id}.png` |
|
||||||
: undefined, |
|
||||||
author: { |
|
||||||
'@type': 'Person', |
|
||||||
name: 'Kamran Ahmed', |
|
||||||
url: 'https://twitter.com/kamranahmedse', |
|
||||||
}, |
|
||||||
publisher: { |
|
||||||
'@type': 'Organization', |
|
||||||
name: 'roadmap.sh', |
|
||||||
logo: { |
|
||||||
'@type': 'ImageObject', |
|
||||||
url: 'https://roadmap.sh/brand-square.png', |
|
||||||
}, |
|
||||||
}, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
const Helmet = (props: HelmetProps) => { |
|
||||||
const { roadmap, title, canonical, description, keywords, noIndex = false } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<NextHead> |
|
||||||
<meta charSet="UTF-8" /> |
|
||||||
|
|
||||||
<title>{title || siteConfig.title}</title> |
|
||||||
<meta |
|
||||||
name="description" |
|
||||||
content={description || siteConfig.description} |
|
||||||
/> |
|
||||||
|
|
||||||
<meta name="author" content={siteConfig.author} /> |
|
||||||
<meta |
|
||||||
name="keywords" |
|
||||||
content={keywords ? keywords.join(',') : siteConfig.keywords.join(',')} |
|
||||||
/> |
|
||||||
|
|
||||||
{noIndex && <meta name="robots" content="noindex" /> } |
|
||||||
|
|
||||||
<meta |
|
||||||
name="viewport" |
|
||||||
content="width=device-width, user-scalable=yes, initial-scale=1.0, maximum-scale=3.0, minimum-scale=1.0" |
|
||||||
/> |
|
||||||
{canonical && <link rel="canonical" href={canonical} />} |
|
||||||
<meta httpEquiv="Content-Language" content="en" /> |
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" /> |
|
||||||
<meta name="twitter:creator" content="@kamranahmedse" /> |
|
||||||
|
|
||||||
<meta property="og:image:width" content="1200" /> |
|
||||||
<meta property="og:image:height" content="630" /> |
|
||||||
<meta property="og:image" content="https://roadmap.sh/og-img.png" /> |
|
||||||
<meta property="og:image:alt" content="roadmap.sh" /> |
|
||||||
<meta property="og:site_name" content="roadmap.sh" /> |
|
||||||
<meta property="og:title" content={title || siteConfig.title} /> |
|
||||||
<meta property="og:description" content={description || siteConfig.description} /> |
|
||||||
<meta property="og:type" content="website" /> |
|
||||||
<meta property="og:url" content="https://roadmap.sh" /> |
|
||||||
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" /> |
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" /> |
|
||||||
<meta |
|
||||||
name="apple-mobile-web-app-status-bar-style" |
|
||||||
content="black-translucent" |
|
||||||
/> |
|
||||||
<link |
|
||||||
rel="apple-touch-icon" |
|
||||||
sizes="180x180" |
|
||||||
href="/manifest/apple-touch-icon.png" |
|
||||||
/> |
|
||||||
<meta name="msapplication-TileColor" content="#101010" /> |
|
||||||
<meta name="theme-color" content="#848a9a" /> |
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest/manifest.json" /> |
|
||||||
<link |
|
||||||
rel="icon" |
|
||||||
type="image/png" |
|
||||||
sizes="32x32" |
|
||||||
href="/manifest/icon32.png" |
|
||||||
/> |
|
||||||
<link |
|
||||||
rel="icon" |
|
||||||
type="image/png" |
|
||||||
sizes="16x16" |
|
||||||
href="/manifest/icon16.png" |
|
||||||
/> |
|
||||||
<link |
|
||||||
rel="shortcut icon" |
|
||||||
href="/manifest/favicon.ico" |
|
||||||
type="image/x-icon" |
|
||||||
/> |
|
||||||
<link rel="icon" href="/manifest/favicon.ico" type="image/x-icon" /> |
|
||||||
|
|
||||||
{roadmap?.id && ( |
|
||||||
<script |
|
||||||
type="application/ld+json" |
|
||||||
dangerouslySetInnerHTML={{ |
|
||||||
__html: JSON.stringify(getRichSnippetJson(roadmap)), |
|
||||||
}} |
|
||||||
/> |
|
||||||
)} |
|
||||||
|
|
||||||
{/* Global Site Tag (gtag.js) - Google Analytics */} |
|
||||||
{process.env.GA_SECRET && ( |
|
||||||
<> |
|
||||||
<script |
|
||||||
async |
|
||||||
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.GA_SECRET}`} |
|
||||||
/> |
|
||||||
<script |
|
||||||
dangerouslySetInnerHTML={{ |
|
||||||
__html: ` |
|
||||||
window.dataLayer = window.dataLayer || []; |
|
||||||
function gtag(){dataLayer.push(arguments);} |
|
||||||
gtag('js', new Date()); |
|
||||||
gtag('config', '${process.env.GA_SECRET}'); |
|
||||||
`,
|
|
||||||
}} |
|
||||||
/> |
|
||||||
</> |
|
||||||
)} |
|
||||||
</NextHead> |
|
||||||
); |
|
||||||
}; |
|
||||||
|
|
||||||
export default Helmet; |
|
@ -1,73 +0,0 @@ |
|||||||
import { RoadmapType } from '../../lib/roadmap'; |
|
||||||
import { SimpleGrid, Tag } from '@chakra-ui/react'; |
|
||||||
import { HomeRoadmapItem } from '../roadmap/home-roadmap-item'; |
|
||||||
|
|
||||||
type FeaturedRoadmapsListProps = { |
|
||||||
roadmaps: RoadmapType[]; |
|
||||||
title: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export const upcomingRoadmaps = [ |
|
||||||
{ |
|
||||||
type: 'Role Based', |
|
||||||
title: 'React Native', |
|
||||||
description: 'Step by step guide to become a React Native Developer', |
|
||||||
id: 'react-native' |
|
||||||
}, |
|
||||||
{ |
|
||||||
type: 'Role Based', |
|
||||||
title: 'Cyber Security', |
|
||||||
description: 'Step by step guide to become a Cyber Security Expert', |
|
||||||
id: 'cyber-security' |
|
||||||
}, |
|
||||||
// {
|
|
||||||
// type: 'Skill Based',
|
|
||||||
// title: 'TypeScript',
|
|
||||||
// description: 'Step by step guide to learn TypeScript in 2022',
|
|
||||||
// id: 'typescript'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'Skill Based',
|
|
||||||
// title: 'Rust',
|
|
||||||
// description: 'Step by step guide to learn Rust in 2022',
|
|
||||||
// id: 'rust'
|
|
||||||
// },
|
|
||||||
]; |
|
||||||
|
|
||||||
export function FeaturedRoadmapsList(props: FeaturedRoadmapsListProps) { |
|
||||||
const { roadmaps, title } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<Tag bg='gray.400' mb={4}>{title}</Tag> |
|
||||||
<SimpleGrid columns={[1, 2, 3]} spacing={['10px', '10px', '15px']} mb='40px'> |
|
||||||
<> |
|
||||||
{roadmaps.map((roadmap: RoadmapType, counter: number) => ( |
|
||||||
<HomeRoadmapItem |
|
||||||
isUpcoming={roadmap.isUpcoming} |
|
||||||
url={`/${roadmap.id}`} |
|
||||||
key={roadmap.id} |
|
||||||
colorIndex={counter} |
|
||||||
title={roadmap.featuredTitle === 'Software Design and Architecture' ? 'Software Design' : roadmap.featuredTitle} |
|
||||||
isCommunity={roadmap.isCommunity} |
|
||||||
isNew={roadmap.isNew} |
|
||||||
subtitle={roadmap.featuredDescription} |
|
||||||
/> |
|
||||||
))} |
|
||||||
{upcomingRoadmaps |
|
||||||
.filter(roadmap => roadmap.type === title) |
|
||||||
.map((roadmap, counter) => ( |
|
||||||
<HomeRoadmapItem |
|
||||||
isUpcoming={true} |
|
||||||
url={`/upcoming?id=${roadmap.id}`} |
|
||||||
key={`upcoming-${roadmap.id}`} |
|
||||||
colorIndex={9} |
|
||||||
title={roadmap.title} |
|
||||||
subtitle={roadmap.description} |
|
||||||
/> |
|
||||||
))} |
|
||||||
</> |
|
||||||
</SimpleGrid> |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
Before Width: | Height: | Size: 507 B |
Before Width: | Height: | Size: 298 B |
Before Width: | Height: | Size: 841 B |
Before Width: | Height: | Size: 515 B |
Before Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 184 B |
Before Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 550 B |
@ -1,21 +0,0 @@ |
|||||||
export function VideoIcon(props: any) { |
|
||||||
return ( |
|
||||||
<svg |
|
||||||
stroke='currentColor' |
|
||||||
fill='currentColor' |
|
||||||
strokeWidth='0' |
|
||||||
viewBox='0 0 24 24' |
|
||||||
height='1em' |
|
||||||
width='1em' |
|
||||||
xmlns='http://www.w3.org/2000/svg' |
|
||||||
{...props} |
|
||||||
> |
|
||||||
<g> |
|
||||||
<path fill='none' d='M0 0h24v24H0z' /> |
|
||||||
<path |
|
||||||
d='M3 3.993C3 3.445 3.445 3 3.993 3h16.014c.548 0 .993.445.993.993v16.014a.994.994 0 0 1-.993.993H3.993A.994.994 0 0 1 3 20.007V3.993zm7.622 4.422a.4.4 0 0 0-.622.332v6.506a.4.4 0 0 0 .622.332l4.879-3.252a.4.4 0 0 0 0-.666l-4.88-3.252z' |
|
||||||
/> |
|
||||||
</g> |
|
||||||
</svg> |
|
||||||
); |
|
||||||
} |
|
Before Width: | Height: | Size: 369 B |
@ -1,58 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Badge, Flex, Link, Text } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type LinksListItemProps = { |
|
||||||
href: string; |
|
||||||
title: string; |
|
||||||
subtitle: string; |
|
||||||
badgeText?: string; |
|
||||||
target?: string; |
|
||||||
icon?: React.ReactChild; |
|
||||||
hideSubtitleOnMobile?: boolean; |
|
||||||
}; |
|
||||||
|
|
||||||
export function LinksListItem(props: LinksListItemProps) { |
|
||||||
const { title, subtitle, badgeText, icon, hideSubtitleOnMobile = false, href, target } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Link |
|
||||||
target={target || '_self'} |
|
||||||
href={href} |
|
||||||
fontSize={['14px', '14px', '15px']} |
|
||||||
py='9px' |
|
||||||
d='flex' |
|
||||||
flexDirection={['column', 'row', 'row']} |
|
||||||
fontWeight={500} |
|
||||||
color='gray.600' |
|
||||||
alignItems={['flex-start', 'center']} |
|
||||||
justifyContent={'space-between'} |
|
||||||
sx={{ |
|
||||||
'@media (hover: none)': { |
|
||||||
'&:hover': { |
|
||||||
'& .list-item-title': { |
|
||||||
transform: 'none' |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}} |
|
||||||
_hover={{ |
|
||||||
textDecoration: 'none', |
|
||||||
color: 'blue.400', |
|
||||||
'& .list-item-title': { |
|
||||||
transform: 'translateX(10px)' |
|
||||||
} |
|
||||||
}} |
|
||||||
isTruncated |
|
||||||
maxWidth='100%' |
|
||||||
> |
|
||||||
<Flex alignItems='center' className='list-item-title' transition={'200ms'}> |
|
||||||
{icon} |
|
||||||
<Text maxWidth={'345px'} isTruncated as='span'>{title}</Text> |
|
||||||
{badgeText && |
|
||||||
<Badge pos='relative' top='1px' variant='subtle' colorScheme='green' ml='10px'>{badgeText}</Badge>} |
|
||||||
</Flex> |
|
||||||
<Text d={[hideSubtitleOnMobile ? 'none' : 'inline', 'inline']} mt={['3px', 0]} as='span' |
|
||||||
fontSize={['11px', '11px', '12px']} color='gray.500'>{subtitle}</Text> |
|
||||||
</Link> |
|
||||||
); |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { StackDivider, VStack } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type LinksListProps = { |
|
||||||
children: React.ReactNode |
|
||||||
}; |
|
||||||
|
|
||||||
export function LinksList(props: LinksListProps) { |
|
||||||
const { children } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<VStack |
|
||||||
rounded='5px' |
|
||||||
divider={<StackDivider borderColor='gray.200' />} |
|
||||||
spacing={0} |
|
||||||
align='stretch' |
|
||||||
> |
|
||||||
{children} |
|
||||||
</VStack> |
|
||||||
); |
|
||||||
} |
|
@ -1,20 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
// @ts-ignore
|
|
||||||
import { MDXProvider } from '@mdx-js/react'; |
|
||||||
import { ChakraProvider } from '@chakra-ui/react'; |
|
||||||
import MdxComponents from './mdx-components'; |
|
||||||
import { roadmapTheme } from '../../styles/theme'; |
|
||||||
|
|
||||||
type MdRendererType = { |
|
||||||
children: React.ReactNode |
|
||||||
}; |
|
||||||
|
|
||||||
export default function MdRenderer(props: MdRendererType) { |
|
||||||
return ( |
|
||||||
<ChakraProvider theme={roadmapTheme} resetCSS> |
|
||||||
<MDXProvider components={MdxComponents}> |
|
||||||
{props.children} |
|
||||||
</MDXProvider> |
|
||||||
</ChakraProvider> |
|
||||||
); |
|
||||||
}; |
|
@ -1,37 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
type EnrichedLinkProps = { |
|
||||||
href: string; |
|
||||||
children: React.ReactNode; |
|
||||||
}; |
|
||||||
|
|
||||||
const Link = styled.a` |
|
||||||
font-weight: 600; |
|
||||||
text-decoration: underline; |
|
||||||
`;
|
|
||||||
|
|
||||||
const EnrichedLink = (props: EnrichedLinkProps) => { |
|
||||||
// Is external URL or is a media URL
|
|
||||||
const isExternalUrl = /(^http(s)?:\/\/)|(\.(png|svg|jpeg|jpg)$)/.test( |
|
||||||
props.href |
|
||||||
); |
|
||||||
|
|
||||||
const linkProps: Record<string, string> = { |
|
||||||
target: '_self', |
|
||||||
...(isExternalUrl |
|
||||||
? { |
|
||||||
rel: 'nofollow', |
|
||||||
target: '_blank', |
|
||||||
} |
|
||||||
: {}), |
|
||||||
}; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Link href={props.href} {...linkProps}> |
|
||||||
{props.children} |
|
||||||
</Link> |
|
||||||
); |
|
||||||
}; |
|
||||||
|
|
||||||
export default EnrichedLink; |
|
@ -1,53 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Link, Text, Badge } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type BadgeLinkType = { |
|
||||||
target: string; |
|
||||||
badgeText: string; |
|
||||||
href: string; |
|
||||||
colorScheme?: string; |
|
||||||
children: React.ReactNode; |
|
||||||
}; |
|
||||||
|
|
||||||
export function BadgeLink(props: BadgeLinkType) { |
|
||||||
const { |
|
||||||
target = '_blank', |
|
||||||
colorScheme = 'purple', |
|
||||||
badgeText, |
|
||||||
href, |
|
||||||
children, |
|
||||||
} = props; |
|
||||||
|
|
||||||
// Is external URL or is a media URL
|
|
||||||
const isExternalUrl = /(^http(s)?:\/\/)|(\.(png|svg|jpeg|jpg)$)/.test( |
|
||||||
props.href |
|
||||||
); |
|
||||||
|
|
||||||
const linkProps: Record<string, string> = { |
|
||||||
...(isExternalUrl |
|
||||||
? { |
|
||||||
rel: 'nofollow', |
|
||||||
} |
|
||||||
: {}), |
|
||||||
}; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Text mb={'0px'}> |
|
||||||
<Link |
|
||||||
fontSize="14px" |
|
||||||
color="blue.700" |
|
||||||
fontWeight={500} |
|
||||||
textDecoration="none" |
|
||||||
href={href} |
|
||||||
target={target} |
|
||||||
_hover={{ textDecoration: 'none', color: 'purple.400' }} |
|
||||||
{...linkProps} |
|
||||||
> |
|
||||||
<Badge fontSize="11px" mr="7px" colorScheme={colorScheme}> |
|
||||||
{badgeText} |
|
||||||
</Badge> |
|
||||||
{children} |
|
||||||
</Link> |
|
||||||
</Text> |
|
||||||
); |
|
||||||
} |
|
@ -1,27 +0,0 @@ |
|||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
const BlockQuote = styled.blockquote` |
|
||||||
padding: 16px 20px; |
|
||||||
position: relative; |
|
||||||
background: #e8e8e8; |
|
||||||
border-radius: 5px; |
|
||||||
margin-bottom: 18px; |
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 { |
|
||||||
margin-top: 0; |
|
||||||
} |
|
||||||
|
|
||||||
p + h4 { |
|
||||||
margin-top: 15px; |
|
||||||
} |
|
||||||
|
|
||||||
p { |
|
||||||
margin: 0; |
|
||||||
|
|
||||||
& + p { |
|
||||||
margin-top: 10px; |
|
||||||
} |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
export default BlockQuote; |
|
@ -1,10 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Code as ChakraCode } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type CodeType = { |
|
||||||
children: React.ReactNode; |
|
||||||
} |
|
||||||
|
|
||||||
export default function Code(props: CodeType) { |
|
||||||
return <ChakraCode bg='blue.500'>{props.children}</ChakraCode>; |
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
import { Box, Flex, Heading, Text } from '@chakra-ui/react'; |
|
||||||
import TreeIcon from '../../icons/tree.svg'; |
|
||||||
|
|
||||||
type DedicatedRoadmapProps = { |
|
||||||
href: string; |
|
||||||
title: string; |
|
||||||
description: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export function DedicatedRoadmap(props: DedicatedRoadmapProps) { |
|
||||||
const { href, title, description } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Flex as={'a'} target='_blank' href={ href } p={5} px={5} mt={6} rounded='md' alignItems='center' _hover={{ bg: 'yellow.400'}} bg='yellow.300'> |
|
||||||
<Box d={['none', 'none', 'none', 'block', 'block']} mr={4} height='32px' w='32px' as={TreeIcon} color='gray.900' /> |
|
||||||
<Box as='span'> |
|
||||||
<Heading fontSize='lg' as={'h4'} mb='2px' color='gray.900'>{ title }</Heading> |
|
||||||
<Text color='gray.700' as='span' fontSize='md'>{ description }</Text> |
|
||||||
</Box> |
|
||||||
</Flex> |
|
||||||
); |
|
||||||
} |
|
@ -1,81 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import styled from 'styled-components'; |
|
||||||
import LinkIcon from 'components/icons/link.svg'; |
|
||||||
|
|
||||||
const linkify = (Component: React.FunctionComponent<any>) => { |
|
||||||
return function EnrichedHeading(props: { children: string }): React.ReactNode { |
|
||||||
const text = props.children; |
|
||||||
const id = text?.toLowerCase && text |
|
||||||
.toLowerCase() |
|
||||||
.replace(/[^\x00-\x7F]/g, '') |
|
||||||
.replace(/\s+/g, '-') |
|
||||||
.replace(/[?!]/g, ''); |
|
||||||
|
|
||||||
return ( |
|
||||||
<Component id={id}> |
|
||||||
<HeaderLink href={`#${id}`}> |
|
||||||
<LinkIcon /> |
|
||||||
</HeaderLink> |
|
||||||
{props.children} |
|
||||||
</Component> |
|
||||||
); |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const HeaderLink = styled.a` |
|
||||||
position: absolute; |
|
||||||
top: 0; |
|
||||||
left: -25px; |
|
||||||
width: 25px; |
|
||||||
display: none; |
|
||||||
height: 100%; |
|
||||||
align-items: center; |
|
||||||
justify-content: flex-start; |
|
||||||
`;
|
|
||||||
|
|
||||||
const H1 = styled.h1` |
|
||||||
position: relative; |
|
||||||
font-size: 32px; |
|
||||||
line-height: 40px; |
|
||||||
font-weight: 700; |
|
||||||
margin: 20px 0 10px !important; |
|
||||||
|
|
||||||
&:hover ${HeaderLink} { |
|
||||||
display: flex; |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
const H2 = styled(H1).attrs({ as: 'h2' })` |
|
||||||
font-size: 30px; |
|
||||||
`;
|
|
||||||
|
|
||||||
const H3 = styled(H1).attrs({ as: 'h3' })` |
|
||||||
margin: 22px 0 8px; |
|
||||||
font-size: 28px; |
|
||||||
`;
|
|
||||||
|
|
||||||
const H4 = styled(H1).attrs({ as: 'h4' })` |
|
||||||
margin: 18px 0 8px; |
|
||||||
font-size: 24px; |
|
||||||
`;
|
|
||||||
|
|
||||||
const H5 = styled(H1).attrs({ as: 'h5' })` |
|
||||||
margin: 14px 0 8px; |
|
||||||
font-size: 18px; |
|
||||||
`;
|
|
||||||
|
|
||||||
const H6 = styled(H1).attrs({ as: 'h6' })` |
|
||||||
margin: 12px 0 8px; |
|
||||||
font-size: 18px; |
|
||||||
`;
|
|
||||||
|
|
||||||
const Headings = { |
|
||||||
h1: H1, |
|
||||||
h2: H2, |
|
||||||
h3: H3, |
|
||||||
h4: H4, |
|
||||||
h5: H5, |
|
||||||
h6: H6 |
|
||||||
}; |
|
||||||
|
|
||||||
export default Headings; |
|
@ -1,47 +0,0 @@ |
|||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
type IFrameProps = { |
|
||||||
title: string; |
|
||||||
src: string; |
|
||||||
}; |
|
||||||
|
|
||||||
const AspectRatioBox = styled.div` |
|
||||||
position: relative; |
|
||||||
max-width: 100%; |
|
||||||
margin-bottom: 18px; |
|
||||||
|
|
||||||
&:before { |
|
||||||
height: 0; |
|
||||||
content: ""; |
|
||||||
display: block; |
|
||||||
padding-bottom: 50%; |
|
||||||
} |
|
||||||
|
|
||||||
& > iframe { |
|
||||||
overflow: hidden; |
|
||||||
position: absolute; |
|
||||||
top: 0; |
|
||||||
right: 0; |
|
||||||
bottom: 0; |
|
||||||
left: 0; |
|
||||||
display: flex; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
width: 100%; |
|
||||||
height: 100%; |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
export default function IFrame(props: IFrameProps) { |
|
||||||
return ( |
|
||||||
<AspectRatioBox> |
|
||||||
<iframe |
|
||||||
frameBorder={0} |
|
||||||
title={props.title} |
|
||||||
src={props.src} |
|
||||||
allow={'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'} |
|
||||||
allowFullScreen |
|
||||||
/> |
|
||||||
</AspectRatioBox> |
|
||||||
); |
|
||||||
} |
|
@ -1,7 +0,0 @@ |
|||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
export const Img = styled.img` |
|
||||||
max-width: 100%; |
|
||||||
margin: 25px auto; |
|
||||||
display: block; |
|
||||||
`;
|
|
@ -1,34 +0,0 @@ |
|||||||
import { Code } from '@chakra-ui/react'; |
|
||||||
import { P } from './p'; |
|
||||||
import Headings from './heading'; |
|
||||||
import { Pre } from './pre'; |
|
||||||
import BlockQuote from './blockquote'; |
|
||||||
import { Table } from './table'; |
|
||||||
import IFrame from './iframe'; |
|
||||||
import { Img } from './img'; |
|
||||||
import EnrichedLink from './a'; |
|
||||||
import { BadgeLink } from './badge-link'; |
|
||||||
import { Li, Ul } from './ul'; |
|
||||||
import PremiumBlock from './premium-block'; |
|
||||||
import { ResourceGroupTitle } from './resource-group-title'; |
|
||||||
import { DedicatedRoadmap } from './dedicated-roadmap'; |
|
||||||
|
|
||||||
const MdxComponents = { |
|
||||||
p: P, |
|
||||||
...Headings, |
|
||||||
pre: Pre, |
|
||||||
blockquote: BlockQuote, |
|
||||||
a: EnrichedLink, |
|
||||||
DedicatedRoadmap, |
|
||||||
table: Table, |
|
||||||
iframe: IFrame, |
|
||||||
img: Img, |
|
||||||
code: Code, |
|
||||||
BadgeLink: BadgeLink, |
|
||||||
ResourceGroupTitle: ResourceGroupTitle, |
|
||||||
PremiumBlock: PremiumBlock, |
|
||||||
ul: Ul, |
|
||||||
li: Li |
|
||||||
}; |
|
||||||
|
|
||||||
export default MdxComponents; |
|
@ -1,14 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Text } from '@chakra-ui/react'; |
|
||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
type EnrichedTextType = { |
|
||||||
children: React.ReactNode; |
|
||||||
} |
|
||||||
|
|
||||||
export const P = styled.p` |
|
||||||
line-height: 27px; |
|
||||||
font-size: 16px; |
|
||||||
color: black; |
|
||||||
margin-bottom: 18px; |
|
||||||
`;
|
|
@ -1,12 +0,0 @@ |
|||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
export const Pre = styled.pre` |
|
||||||
margin: 25px -25px 25px -25px !important; |
|
||||||
padding: 20px 25px !important; |
|
||||||
border-radius: 10px; |
|
||||||
line-height: 1.5 !important; |
|
||||||
|
|
||||||
code { |
|
||||||
background: transparent; |
|
||||||
} |
|
||||||
`;
|
|
@ -1,19 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Box, Button, Heading, Text } from '@chakra-ui/react'; |
|
||||||
import { LockIcon } from '@chakra-ui/icons'; |
|
||||||
|
|
||||||
type PremiumBlockProps = { |
|
||||||
title: string; |
|
||||||
description: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export default function PremiumBlock(props: PremiumBlockProps) { |
|
||||||
return ( |
|
||||||
<Box p='40px' textAlign='center' rounded='5px' mb='18px' bg='gray.50' borderWidth={1}> |
|
||||||
<LockIcon color='gray.300' height='45px' w='45px' mb='18px' /> |
|
||||||
<Heading as='h3' fontSize='30px' mb='10px'>{props.title}</Heading> |
|
||||||
<Text mb='18px'>{props.description}</Text> |
|
||||||
<Button colorScheme='green'>Become a Member</Button> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Heading } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type ResourceGroupTitleProps = { |
|
||||||
children: React.ReactNode; |
|
||||||
}; |
|
||||||
|
|
||||||
export function ResourceGroupTitle(props: ResourceGroupTitleProps) { |
|
||||||
const { children } = props; |
|
||||||
|
|
||||||
return <Heading mt='20px' color='gray.800' fontSize='14px' pb='5px' borderBottomWidth={1} textTransform='uppercase' as="h2" mb={'10px'}>{children}</Heading>; |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
export const Table = styled.table` |
|
||||||
border-collapse: separate; |
|
||||||
width: 100%; |
|
||||||
border-spacing: 0; |
|
||||||
margin: 20px 0; |
|
||||||
|
|
||||||
th { |
|
||||||
color: #666; |
|
||||||
font-size: 12px; |
|
||||||
font-weight: 400; |
|
||||||
background: #FAFAFA; |
|
||||||
text-transform: uppercase; |
|
||||||
height: 40px; |
|
||||||
vertical-align: middle; |
|
||||||
padding: 5px 10px; |
|
||||||
} |
|
||||||
|
|
||||||
td { |
|
||||||
font-size: 14px; |
|
||||||
padding: 10px; |
|
||||||
border-bottom: 1px solid #EAEAEA; |
|
||||||
} |
|
||||||
`;
|
|
@ -1,16 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { UnorderedList } from '@chakra-ui/react'; |
|
||||||
import styled from 'styled-components'; |
|
||||||
|
|
||||||
export const Ul = styled.ul` |
|
||||||
margin-left: 40px; |
|
||||||
margin-bottom: 18px; |
|
||||||
|
|
||||||
ul { |
|
||||||
margin-top: 18px; |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
export const Li = styled.li` |
|
||||||
margin-bottom: 7px; |
|
||||||
`;
|
|
@ -1,29 +0,0 @@ |
|||||||
import { Box, Container, Heading, Link, Text } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
export function OpensourceBanner() { |
|
||||||
return ( |
|
||||||
<Box bg='white' borderTopWidth={1} py={['45px', '45px', '70px']} textAlign='center'> |
|
||||||
<Container maxW='container.sm'> |
|
||||||
<Heading fontSize={['25px', '25px', '35px']} mb={['10px', '10px', '20px']}>Open Source</Heading> |
|
||||||
<Text lineHeight='26px' fontSize={['15px', '15px', '16px']} mb='20px'>The project is OpenSource, |
|
||||||
<Link |
|
||||||
_hover={{ textDecoration: 'none' }} |
|
||||||
href='https://github.com/search?o=desc&q=stars%3A%3E100000&s=stars&type=Repositories' |
|
||||||
target='_blank' |
|
||||||
borderBottomWidth={1} |
|
||||||
fontWeight={600} |
|
||||||
>6th most starred project on GitHub</Link> and is visited by hundreds of thousands of |
|
||||||
developers every month.</Text> |
|
||||||
<iframe |
|
||||||
src='https://ghbtns.com/github-btn.html?user=kamranahmedse&repo=developer-roadmap&type=star&count=true&size=large' |
|
||||||
frameBorder='0' |
|
||||||
scrolling='0' |
|
||||||
width='170' |
|
||||||
height='30' |
|
||||||
style={{ margin: 'auto' }} |
|
||||||
title='GitHub' |
|
||||||
/> |
|
||||||
</Container> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,37 +0,0 @@ |
|||||||
import { Box, Container, Heading, Text } from '@chakra-ui/react'; |
|
||||||
import React from 'react'; |
|
||||||
|
|
||||||
type PageHeaderProps = { |
|
||||||
title: string; |
|
||||||
subtitle: string; |
|
||||||
children?: React.ReactNode; |
|
||||||
beforeTitle?: React.ReactNode; |
|
||||||
}; |
|
||||||
|
|
||||||
export function PageHeader(props: PageHeaderProps) { |
|
||||||
const { title, subtitle, children, beforeTitle = null } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box pt={['25px', '20px', '45px']} pb={['20px', '15px', '30px']} borderBottomWidth={1} mb='30px'> |
|
||||||
<Container maxW='container.md' position='relative'> |
|
||||||
{beforeTitle} |
|
||||||
<Heading |
|
||||||
as='h1' |
|
||||||
color='black' |
|
||||||
fontSize={['28px', '33px', '40px']} |
|
||||||
fontWeight={700} |
|
||||||
mb={['2px', '2px', '5px']} |
|
||||||
> |
|
||||||
{title} |
|
||||||
</Heading> |
|
||||||
<Text fontSize={['13px', '14px', '15px']}>{subtitle}</Text> |
|
||||||
</Container> |
|
||||||
|
|
||||||
{children && ( |
|
||||||
<Container maxW='container.md'> |
|
||||||
{children} |
|
||||||
</Container> |
|
||||||
)} |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Box } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type PageWrapperProps = { |
|
||||||
children: React.ReactNode; |
|
||||||
} |
|
||||||
|
|
||||||
export function PageWrapper(props: PageWrapperProps) { |
|
||||||
const { children } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box bgColor='brand.bg' bgImage='url(/bg.jpg)' bgRepeat='no-repeat' bgSize='100%' w='100%' minH='100vh'> |
|
||||||
{ children } |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,80 +0,0 @@ |
|||||||
import { Badge, Box, Button, Container, Link, Stack, Text } from '@chakra-ui/react'; |
|
||||||
import { RoadmapType } from '../lib/roadmap'; |
|
||||||
|
|
||||||
type RelatedRoadmapsProps = { |
|
||||||
roadmaps: RoadmapType[]; |
|
||||||
}; |
|
||||||
|
|
||||||
const colorsList = [ |
|
||||||
'gray.700', |
|
||||||
'purple.700', |
|
||||||
'blue.700', |
|
||||||
'red.700', |
|
||||||
'green.700', |
|
||||||
'teal.700', |
|
||||||
'yellow.700', |
|
||||||
'cyan.700', |
|
||||||
'pink.700' |
|
||||||
]; |
|
||||||
|
|
||||||
const roadmapTitleMapping: Record<string, string> = { |
|
||||||
"Software Design and Architecture": "Software Design", |
|
||||||
} |
|
||||||
|
|
||||||
export function RelatedRoadmaps(props: RelatedRoadmapsProps) { |
|
||||||
const { roadmaps } = props; |
|
||||||
if (!roadmaps.length) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box borderTopWidth={1} bgColor='gray.50' pb='35px' pt='5px'> |
|
||||||
<Container maxW='container.md'> |
|
||||||
<Box display='flex' position='relative' top='-23px' alignItems='center' justifyContent='space-between'> |
|
||||||
<Text textAlign='center' borderWidth={1} bg='white' p='4px' fontWeight='bold' rounded='md' px={'15px'}> |
|
||||||
Related Roadmaps |
|
||||||
</Text> |
|
||||||
|
|
||||||
<Button as={Link} variant='outline' bg='white' size='sm' _hover={{ textDecoration: 'none', bg: 'gray.100' }} |
|
||||||
href='/'> |
|
||||||
<Text as='span' display={['inline', 'none', 'none']}>More →</Text> |
|
||||||
<Text as='span' display={['none', 'inline', 'inline']}>All Roadmaps →</Text> |
|
||||||
</Button> |
|
||||||
</Box> |
|
||||||
|
|
||||||
<Stack spacing='5px'> |
|
||||||
{roadmaps.map((roadmap, counter) => ( |
|
||||||
<Link |
|
||||||
href={`/${roadmap.id}`} |
|
||||||
key={roadmap.id} |
|
||||||
borderWidth={1} |
|
||||||
borderColor='blue.100' |
|
||||||
py='7px' |
|
||||||
px='14px' |
|
||||||
rounded='md' |
|
||||||
bg='white' |
|
||||||
textDecoration={'none'} |
|
||||||
_hover={{ bg: 'gray.100', borderColor: 'blue.200' }} |
|
||||||
bgGradient='linear(to-r, white, gray.50)' |
|
||||||
display='flex' |
|
||||||
alignItems='center' |
|
||||||
flexDir={['column', 'row', 'row']} |
|
||||||
> |
|
||||||
<Text |
|
||||||
color={colorsList[counter]} |
|
||||||
as='span' |
|
||||||
fontWeight='bold' |
|
||||||
display={['inline-block']} |
|
||||||
minWidth='150px' |
|
||||||
mr='10px' |
|
||||||
> |
|
||||||
{roadmapTitleMapping[roadmap.featuredTitle] || roadmap.featuredTitle} |
|
||||||
</Text> |
|
||||||
<Text as='span' display={['block', 'inline']} isTruncated maxWidth='100%' fontSize={['sm', 'sm', 'md']} color='gray.700'>{roadmap.featuredDescription}</Text> |
|
||||||
</Link> |
|
||||||
))} |
|
||||||
</Stack> |
|
||||||
</Container> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,131 +0,0 @@ |
|||||||
import { Box, Button, Flex, Text } from '@chakra-ui/react'; |
|
||||||
import { RemoveScroll } from 'react-remove-scroll'; |
|
||||||
import { RoadmapType } from '../../lib/roadmap'; |
|
||||||
import RoadmapGroup from '../../pages/[roadmap]/[group]'; |
|
||||||
import { CheckIcon, CloseIcon, RepeatIcon } from '@chakra-ui/icons'; |
|
||||||
import { queryGroupElementsById } from '../../lib/renderer'; |
|
||||||
|
|
||||||
type ContentDrawerProps = { |
|
||||||
roadmap: RoadmapType; |
|
||||||
groupId: string; |
|
||||||
onClose?: () => void; |
|
||||||
}; |
|
||||||
|
|
||||||
export function markTopicDone(groupId: string) { |
|
||||||
localStorage.setItem(groupId, 'done'); |
|
||||||
|
|
||||||
queryGroupElementsById(groupId).forEach((item) => |
|
||||||
item?.classList?.add('done') |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function markTopicPending(groupId: string) { |
|
||||||
localStorage.removeItem(groupId); |
|
||||||
|
|
||||||
queryGroupElementsById(groupId).forEach((item) => |
|
||||||
item?.classList?.remove('done') |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function isTopicDone(groupId: string) { |
|
||||||
return localStorage.getItem(groupId) === 'done'; |
|
||||||
} |
|
||||||
|
|
||||||
export function ContentDrawer(props: ContentDrawerProps) { |
|
||||||
const { roadmap, groupId, onClose = () => null } = props; |
|
||||||
if (!groupId) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
const isDone = isTopicDone(groupId); |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box zIndex={99999} pos="relative"> |
|
||||||
<Box |
|
||||||
onClick={onClose} |
|
||||||
pos="fixed" |
|
||||||
top={0} |
|
||||||
left={0} |
|
||||||
right={0} |
|
||||||
bottom={0} |
|
||||||
bg="black" |
|
||||||
opacity={0.4} |
|
||||||
/> |
|
||||||
<RemoveScroll allowPinchZoom> |
|
||||||
<Box |
|
||||||
p="0px 30px 30px" |
|
||||||
position="fixed" |
|
||||||
w={['100%', '60%', '40%']} |
|
||||||
bg="white" |
|
||||||
top={0} |
|
||||||
right={0} |
|
||||||
bottom={0} |
|
||||||
borderLeftWidth={'1px'} |
|
||||||
overflowY="scroll" |
|
||||||
> |
|
||||||
<Flex |
|
||||||
mt="20px" |
|
||||||
justifyContent="space-between" |
|
||||||
alignItems="center" |
|
||||||
zIndex={1} |
|
||||||
> |
|
||||||
{!isDone && ( |
|
||||||
<Button |
|
||||||
onClick={() => { |
|
||||||
markTopicDone(groupId); |
|
||||||
onClose(); |
|
||||||
}} |
|
||||||
colorScheme="green" |
|
||||||
leftIcon={<CheckIcon />} |
|
||||||
size="xs" |
|
||||||
iconSpacing={0} |
|
||||||
> |
|
||||||
<Text |
|
||||||
as="span" |
|
||||||
d={['block', 'none', 'none', 'block']} |
|
||||||
ml="10px" |
|
||||||
> |
|
||||||
Mark as Done |
|
||||||
</Text> |
|
||||||
</Button> |
|
||||||
)} |
|
||||||
{isDone && ( |
|
||||||
<Button |
|
||||||
onClick={() => { |
|
||||||
markTopicPending(groupId); |
|
||||||
onClose(); |
|
||||||
}} |
|
||||||
colorScheme="red" |
|
||||||
leftIcon={<RepeatIcon />} |
|
||||||
size="xs" |
|
||||||
iconSpacing={0} |
|
||||||
> |
|
||||||
<Text |
|
||||||
as="span" |
|
||||||
d={['block', 'none', 'none', 'block']} |
|
||||||
ml="10px" |
|
||||||
> |
|
||||||
Mark as Pending |
|
||||||
</Text> |
|
||||||
</Button> |
|
||||||
)} |
|
||||||
<Button |
|
||||||
onClick={onClose} |
|
||||||
colorScheme="yellow" |
|
||||||
ml="5px" |
|
||||||
leftIcon={<CloseIcon width="8px" />} |
|
||||||
iconSpacing={0} |
|
||||||
size="xs" |
|
||||||
> |
|
||||||
<Text as="span" d={['none', 'none', 'none', 'block']} ml="10px"> |
|
||||||
Close |
|
||||||
</Text> |
|
||||||
</Button> |
|
||||||
</Flex> |
|
||||||
|
|
||||||
<RoadmapGroup isOutlet roadmap={roadmap} group={groupId} /> |
|
||||||
</Box> |
|
||||||
</RemoveScroll> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,39 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Box, Button, Divider, Link, Text } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type EditContentPageLinkProps = { |
|
||||||
href: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export function EditContentPageLink(props: EditContentPageLinkProps) { |
|
||||||
const { href } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box my='30px'> |
|
||||||
<Divider mb="15px" orientation="horizontal" /> |
|
||||||
<Text |
|
||||||
lineHeight="23px" |
|
||||||
fontWeight={500} |
|
||||||
fontSize="14px" |
|
||||||
color="gray.500" |
|
||||||
mb="10px" |
|
||||||
> |
|
||||||
This page is a work in progress. Help us by writing a small |
|
||||||
introduction to the topic and suggesting a few links to read more |
|
||||||
about this topic. |
|
||||||
</Text> |
|
||||||
<Button |
|
||||||
size="sm" |
|
||||||
py="20px" |
|
||||||
as={Link} |
|
||||||
href={href} |
|
||||||
target="_blank" |
|
||||||
isFullWidth |
|
||||||
colorScheme={'gray'} |
|
||||||
_hover={{ textDecoration: 'none', bg: 'gray.200' }} |
|
||||||
> |
|
||||||
Edit this Page |
|
||||||
</Button> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,121 +0,0 @@ |
|||||||
import { Badge, Box, Flex, Heading, Link, Text, Tooltip } from '@chakra-ui/react'; |
|
||||||
import { InfoIcon } from '@chakra-ui/icons'; |
|
||||||
|
|
||||||
type RoadmapGridItemProps = { |
|
||||||
title: string; |
|
||||||
subtitle: string; |
|
||||||
isCommunity?: boolean; |
|
||||||
isUpcoming?: boolean; |
|
||||||
colorIndex?: number; |
|
||||||
isNew?: boolean; |
|
||||||
url: string; |
|
||||||
}; |
|
||||||
|
|
||||||
const bgColorList = [ |
|
||||||
'red.100', |
|
||||||
'yellow.100', |
|
||||||
'green.200', |
|
||||||
'teal.200', |
|
||||||
'blue.200', |
|
||||||
'red.200', |
|
||||||
'gray.200', |
|
||||||
'teal.200', |
|
||||||
'yellow.100', |
|
||||||
'green.200', |
|
||||||
'red.200' |
|
||||||
]; |
|
||||||
|
|
||||||
export function HomeRoadmapItem(props: RoadmapGridItemProps) { |
|
||||||
const { |
|
||||||
title, |
|
||||||
subtitle, |
|
||||||
isCommunity, |
|
||||||
isNew, |
|
||||||
colorIndex = 0, |
|
||||||
url, |
|
||||||
isUpcoming |
|
||||||
} = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box |
|
||||||
position='relative' |
|
||||||
as={Link} |
|
||||||
href={url} |
|
||||||
_hover={{ |
|
||||||
textDecoration: 'none', |
|
||||||
bg: 'rgba(255,255,255,.10)' |
|
||||||
}} |
|
||||||
sx={{ |
|
||||||
// On mobile devices, don't change the scale
|
|
||||||
'@media (hover: none)': { |
|
||||||
'&:hover': { |
|
||||||
bg: 'rgba(255,255,255,.05)' |
|
||||||
} |
|
||||||
} |
|
||||||
}} |
|
||||||
flex={1} |
|
||||||
shadow='2xl' |
|
||||||
className={'home-roadmap-item'} |
|
||||||
bg={'rgba(255,255,255,.05)'} |
|
||||||
color='white' |
|
||||||
p='15px' |
|
||||||
rounded='10px' |
|
||||||
pos='relative' |
|
||||||
> |
|
||||||
{isCommunity && ( |
|
||||||
<Tooltip label={'Community contribution'} hasArrow placement='top'> |
|
||||||
<InfoIcon opacity={0.5} position='absolute' top='10px' right='10px' /> |
|
||||||
</Tooltip> |
|
||||||
)} |
|
||||||
|
|
||||||
<Heading |
|
||||||
fontSize={['17px', '17px', '22px']} |
|
||||||
color={bgColorList[colorIndex]} |
|
||||||
mb='5px' |
|
||||||
d='flex' |
|
||||||
alignItems='center' |
|
||||||
> |
|
||||||
{title} |
|
||||||
|
|
||||||
{ isNew && <Badge position='absolute' bottom={0} right={0} colorScheme='yellow' ml='10px'>New</Badge> } |
|
||||||
</Heading> |
|
||||||
<Text color='gray.200' fontSize={['13px']}> |
|
||||||
{subtitle} |
|
||||||
</Text> |
|
||||||
|
|
||||||
{isUpcoming && ( |
|
||||||
<Flex |
|
||||||
alignItems='center' |
|
||||||
justifyContent='center' |
|
||||||
pos='absolute' |
|
||||||
left={0} |
|
||||||
right={0} |
|
||||||
top={0} |
|
||||||
bottom={0} |
|
||||||
rounded='10px' |
|
||||||
> |
|
||||||
<Text |
|
||||||
color='white' |
|
||||||
bg='gray.600' |
|
||||||
zIndex={1} |
|
||||||
fontWeight={600} |
|
||||||
p={'5px 10px'} |
|
||||||
rounded='10px' |
|
||||||
> |
|
||||||
Upcoming |
|
||||||
</Text> |
|
||||||
<Box |
|
||||||
bg={'black'} |
|
||||||
pos='absolute' |
|
||||||
top={0} |
|
||||||
left={0} |
|
||||||
right={0} |
|
||||||
bottom={0} |
|
||||||
rounded={'10px'} |
|
||||||
opacity={0.5} |
|
||||||
/> |
|
||||||
</Flex> |
|
||||||
)} |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,50 +0,0 @@ |
|||||||
import { Badge, Link, Text } from '@chakra-ui/react'; |
|
||||||
import siteConfig from '../../content/site.json'; |
|
||||||
import { event } from '../../lib/gtag'; |
|
||||||
import React from 'react'; |
|
||||||
|
|
||||||
export function NewAlertBanner() { |
|
||||||
return ( |
|
||||||
<Text |
|
||||||
_hover={{ |
|
||||||
textDecoration: 'none', |
|
||||||
color: 'blue.700', |
|
||||||
'& .new-badge': { bg: 'blue.700' }, |
|
||||||
}} |
|
||||||
as={Link} |
|
||||||
href={siteConfig.url.youtube} |
|
||||||
d="block" |
|
||||||
target="_blank" |
|
||||||
color="red.700" |
|
||||||
fontSize="sm" |
|
||||||
mb="10px" |
|
||||||
fontWeight={500} |
|
||||||
onClick={() => |
|
||||||
event({ |
|
||||||
category: 'Subscription', |
|
||||||
action: 'Clicked the YouTube banner', |
|
||||||
label: 'YouTube Alert on Roadmap', |
|
||||||
}) |
|
||||||
} |
|
||||||
> |
|
||||||
<Badge |
|
||||||
transition={'all 300ms'} |
|
||||||
className="new-badge" |
|
||||||
mr="7px" |
|
||||||
colorScheme="red" |
|
||||||
variant="solid" |
|
||||||
> |
|
||||||
New |
|
||||||
</Badge> |
|
||||||
<Text textDecoration="underline" as="span" d={['none', 'inline']}> |
|
||||||
Roadmap topics to be covered on our YouTube Channel |
|
||||||
</Text> |
|
||||||
<Text textDecoration="underline" as="span" d={['inline', 'none']}> |
|
||||||
Topic videos being made on YouTube |
|
||||||
</Text> |
|
||||||
<Text as="span" ml="5px"> |
|
||||||
» |
|
||||||
</Text> |
|
||||||
</Text> |
|
||||||
); |
|
||||||
} |
|
@ -1,26 +0,0 @@ |
|||||||
import { RoadmapType } from '../../lib/roadmap'; |
|
||||||
import { Container, Heading, Link, Text } from '@chakra-ui/react'; |
|
||||||
import siteConfig from '../../content/site.json'; |
|
||||||
|
|
||||||
type RoadmapProps = { |
|
||||||
roadmap: RoadmapType; |
|
||||||
}; |
|
||||||
|
|
||||||
export function RoadmapError(props: RoadmapProps) { |
|
||||||
const { roadmap } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Container |
|
||||||
bg={'red.600'} |
|
||||||
maxW={'container.md'} |
|
||||||
position="relative" |
|
||||||
mt="50px" |
|
||||||
p='20px' |
|
||||||
rounded='5px' |
|
||||||
color='white' |
|
||||||
> |
|
||||||
<Heading mb='4px' size='md'>Oops! There's an error</Heading> |
|
||||||
<Text>Try refreshing or <Link target='_blank' fontWeight={700} textDecoration={'underline'} fontSize='14px' href={siteConfig.url.issue}>report a bug</Link> and use the <Link fontWeight={700} textDecoration={'underline'} href={`/roadmaps/${roadmap.id}.png`}>non-interactive version</Link></Text> |
|
||||||
</Container> |
|
||||||
); |
|
||||||
} |
|
@ -1,89 +0,0 @@ |
|||||||
import { Badge, Box, Flex, Heading, Link, Text, Tooltip } from '@chakra-ui/react'; |
|
||||||
import { InfoIcon } from '@chakra-ui/icons'; |
|
||||||
|
|
||||||
type RoadmapGridItemProps = { |
|
||||||
title: string; |
|
||||||
subtitle: string; |
|
||||||
href: string; |
|
||||||
isCommunity?: boolean; |
|
||||||
isUpcoming?: boolean; |
|
||||||
colorIndex?: number; |
|
||||||
}; |
|
||||||
|
|
||||||
const bgColorList = [ |
|
||||||
'gray.900', |
|
||||||
'purple.900', |
|
||||||
'blue.900', |
|
||||||
'red.900', |
|
||||||
'green.900', |
|
||||||
'teal.900', |
|
||||||
'yellow.900', |
|
||||||
'cyan.900', |
|
||||||
'pink.900', |
|
||||||
|
|
||||||
'gray.800', |
|
||||||
'purple.800', |
|
||||||
'blue.800', |
|
||||||
'red.800', |
|
||||||
'green.800', |
|
||||||
'teal.800', |
|
||||||
'yellow.800', |
|
||||||
'cyan.800', |
|
||||||
'pink.800', |
|
||||||
|
|
||||||
'gray.700', |
|
||||||
'purple.700', |
|
||||||
'blue.700', |
|
||||||
'red.700', |
|
||||||
'green.700', |
|
||||||
'teal.700', |
|
||||||
'yellow.700', |
|
||||||
'cyan.700', |
|
||||||
'pink.700', |
|
||||||
|
|
||||||
'gray.600', |
|
||||||
'purple.600', |
|
||||||
'blue.600', |
|
||||||
'red.600', |
|
||||||
'green.600', |
|
||||||
'teal.600', |
|
||||||
'yellow.600', |
|
||||||
'cyan.600', |
|
||||||
'pink.600' |
|
||||||
]; |
|
||||||
|
|
||||||
export function RoadmapGridItem(props: RoadmapGridItemProps) { |
|
||||||
const { title, subtitle, isCommunity = false, isUpcoming = false, colorIndex = 0, href = '/' } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box _hover={{ textDecoration: 'none', transform: 'scale(1.02)' }} as={Link} href={href} shadow='xl' p='20px' |
|
||||||
rounded='10px' bg={bgColorList[colorIndex] ?? bgColorList[0]} flex={1} pos='relative'> |
|
||||||
|
|
||||||
{isCommunity && ( |
|
||||||
<Tooltip label={'Community contribution'} hasArrow placement='top'> |
|
||||||
<InfoIcon opacity={0.5} color='gray.100' position='absolute' top='10px' right='10px' /> |
|
||||||
</Tooltip> |
|
||||||
)} |
|
||||||
|
|
||||||
<Heading color='white' mb={'6px'} fontSize='20px'>{title}</Heading> |
|
||||||
<Text color='gray.300' fontSize='14px'>{subtitle}</Text> |
|
||||||
|
|
||||||
{isUpcoming && ( |
|
||||||
<Flex |
|
||||||
alignItems='center' |
|
||||||
justifyContent='center' |
|
||||||
pos='absolute' |
|
||||||
left={0} |
|
||||||
right={0} |
|
||||||
top={0} |
|
||||||
bottom={0} |
|
||||||
rounded='10px' |
|
||||||
> |
|
||||||
<Text color='white' bg='yellow.900' zIndex={1} fontWeight={600} p={'5px 10px'} |
|
||||||
rounded='10px'>Upcoming</Text> |
|
||||||
<Box bg={'black'} pos='absolute' top={0} left={0} right={0} bottom={0} rounded={'10px'} opacity={0.5} /> |
|
||||||
</Flex> |
|
||||||
)} |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,20 +0,0 @@ |
|||||||
import { Container, Spinner } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
export function RoadmapLoader() { |
|
||||||
return ( |
|
||||||
<Container |
|
||||||
maxW={'container.md'} |
|
||||||
position="relative" |
|
||||||
mt="60px" |
|
||||||
textAlign="center" |
|
||||||
> |
|
||||||
<Spinner |
|
||||||
thickness="7px" |
|
||||||
speed="0.65s" |
|
||||||
emptyColor="gray.200" |
|
||||||
color="gray.500" |
|
||||||
size="xl" |
|
||||||
/> |
|
||||||
</Container> |
|
||||||
); |
|
||||||
} |
|
@ -1,223 +0,0 @@ |
|||||||
import siteConfig from '../../content/site.json'; |
|
||||||
import { isInteractiveRoadmap, RoadmapType } from '../../lib/roadmap'; |
|
||||||
import { NewAlertBanner } from './new-alert-banner'; |
|
||||||
import { |
|
||||||
Badge, |
|
||||||
Box, |
|
||||||
Button, |
|
||||||
Container, |
|
||||||
Flex, |
|
||||||
Heading, |
|
||||||
Input, |
|
||||||
Link, |
|
||||||
Modal, |
|
||||||
ModalBody, |
|
||||||
ModalCloseButton, |
|
||||||
ModalContent, |
|
||||||
ModalOverlay, |
|
||||||
Stack, |
|
||||||
Text, |
|
||||||
useDisclosure |
|
||||||
} from '@chakra-ui/react'; |
|
||||||
import { AtSignIcon, ChatIcon, DownloadIcon } from '@chakra-ui/icons'; |
|
||||||
import React from 'react'; |
|
||||||
import { SIGNUP_EMAIL_INPUT_NAME, SIGNUP_FORM_ACTION } from '../../pages/signup'; |
|
||||||
import { event } from '../../lib/gtag'; |
|
||||||
import { TNSAlert } from './tns-alert'; |
|
||||||
|
|
||||||
type RoadmapPageHeaderType = { |
|
||||||
roadmap: RoadmapType; |
|
||||||
}; |
|
||||||
|
|
||||||
function RoadmapDownloader({ roadmapTitle }: { roadmapTitle: string }) { |
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure(); |
|
||||||
const initialRef = React.useRef(null); |
|
||||||
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<Button |
|
||||||
onClick={(e) => { |
|
||||||
event({ |
|
||||||
category: 'Subscription', |
|
||||||
action: `Clicked Download ${roadmapTitle} Roadmap`, |
|
||||||
label: `Download ${roadmapTitle} Roadmap Button` |
|
||||||
}); |
|
||||||
onOpen(); |
|
||||||
}} |
|
||||||
size='xs' |
|
||||||
py='14px' |
|
||||||
px='10px' |
|
||||||
leftIcon={<DownloadIcon />} |
|
||||||
display={['none', 'flex']} |
|
||||||
colorScheme='yellow' |
|
||||||
variant='solid' |
|
||||||
_hover={{ textDecoration: 'none' }} |
|
||||||
_focus={{ boxShadow: 'none' }} |
|
||||||
> |
|
||||||
Download |
|
||||||
</Button> |
|
||||||
|
|
||||||
<Modal initialFocusRef={initialRef} closeOnOverlayClick={true} isOpen={isOpen} onClose={onClose} isCentered motionPreset='none'> |
|
||||||
<ModalOverlay /> |
|
||||||
<ModalContent> |
|
||||||
<ModalCloseButton /> |
|
||||||
<ModalBody p={6}> |
|
||||||
<Heading mb='5px' fontSize='2xl'>Download Roadmap</Heading> |
|
||||||
<Text fontSize={'md'} color='gray.700'>Enter your email below to receive the download link.</Text> |
|
||||||
<form action={SIGNUP_FORM_ACTION} method='post' target='_blank' onSubmit={() => { |
|
||||||
event({ |
|
||||||
category: 'Subscription', |
|
||||||
action: `Submitted Download ${roadmapTitle} Roadmap Email`, |
|
||||||
label: `PDF / Subscribe ${roadmapTitle} Roadmap` |
|
||||||
}); |
|
||||||
|
|
||||||
onClose(); |
|
||||||
}}> |
|
||||||
<Input required ref={initialRef} size='md' my='10px' type='email' placeholder='Email address' name={SIGNUP_EMAIL_INPUT_NAME} /> |
|
||||||
<Button type='submit' colorScheme='green' size='md' width={'full'}>Send Link</Button> |
|
||||||
</form> |
|
||||||
</ModalBody> |
|
||||||
</ModalContent> |
|
||||||
</Modal> |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
function RoadmapSubscriber({ roadmapTitle }: { roadmapTitle: string }) { |
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure(); |
|
||||||
const initialRef = React.useRef(null); |
|
||||||
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<Button |
|
||||||
onClick={(e) => { |
|
||||||
event({ |
|
||||||
category: 'Subscription', |
|
||||||
action: `Clicked Subscribe ${roadmapTitle} Roadmap`, |
|
||||||
label: `Subscribe ${roadmapTitle} Roadmap Button` |
|
||||||
}); |
|
||||||
onOpen(); |
|
||||||
}} |
|
||||||
size='xs' |
|
||||||
py='14px' |
|
||||||
px='10px' |
|
||||||
leftIcon={<AtSignIcon />} |
|
||||||
display={'flex'} |
|
||||||
colorScheme='yellow' |
|
||||||
variant='solid' |
|
||||||
_hover={{ textDecoration: 'none' }} |
|
||||||
_focus={{ boxShadow: 'none' }} |
|
||||||
> |
|
||||||
Subscribe |
|
||||||
</Button> |
|
||||||
|
|
||||||
<Modal initialFocusRef={initialRef} closeOnOverlayClick={true} isOpen={isOpen} onClose={onClose} isCentered motionPreset='none'> |
|
||||||
<ModalOverlay /> |
|
||||||
<ModalContent> |
|
||||||
<ModalCloseButton /> |
|
||||||
<ModalBody p={6}> |
|
||||||
<Heading mb='5px' fontSize='2xl'>Subscribe</Heading> |
|
||||||
<Text fontSize={'md'} color='gray.700'>Enter your email below to receive updates to this roadmap.</Text> |
|
||||||
<form action={SIGNUP_FORM_ACTION} method='post' target='_blank' onSubmit={() => { |
|
||||||
event({ |
|
||||||
category: 'Subscription', |
|
||||||
action: `Submitted Subscribe ${roadmapTitle} Roadmap Email`, |
|
||||||
label: `Email / Subscribe ${roadmapTitle} Roadmap` |
|
||||||
}); |
|
||||||
|
|
||||||
onClose(); |
|
||||||
}}> |
|
||||||
<Input required ref={initialRef} size='md' my='10px' type='email' placeholder='Email address' name={SIGNUP_EMAIL_INPUT_NAME} /> |
|
||||||
<Button type='submit' colorScheme='green' size='md' width={'full'}>Subscribe</Button> |
|
||||||
</form> |
|
||||||
</ModalBody> |
|
||||||
</ModalContent> |
|
||||||
</Modal> |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function RoadmapPageHeader(props: RoadmapPageHeaderType) { |
|
||||||
const { roadmap } = props; |
|
||||||
|
|
||||||
const hasTNSAlert = ['frontend', 'backend', 'devops'].includes(roadmap.id); |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box |
|
||||||
pt={['25px', '20px', '45px']} |
|
||||||
pb={['20px', '15px', '30px']} |
|
||||||
borderBottomWidth={1} |
|
||||||
mb='50px' |
|
||||||
> |
|
||||||
<Container maxW='container.md' position='relative'> |
|
||||||
<NewAlertBanner /> |
|
||||||
<Heading |
|
||||||
as='h1' |
|
||||||
color='black' |
|
||||||
fontSize={['28px', '33px', '40px']} |
|
||||||
fontWeight={700} |
|
||||||
mb={['2px', '2px', '5px']} |
|
||||||
> |
|
||||||
{roadmap.title} |
|
||||||
</Heading> |
|
||||||
<Text fontSize={['13px', '14px', '15px']}>{roadmap.description}</Text> |
|
||||||
<Flex justifyContent='space-between' alignItems={'center'} mt='20px'> |
|
||||||
<Stack isInline flex={1}> |
|
||||||
<Button |
|
||||||
display={['flex', 'flex']} |
|
||||||
as={Link} |
|
||||||
href={'/roadmaps'} |
|
||||||
size='xs' |
|
||||||
py='14px' |
|
||||||
px='10px' |
|
||||||
colorScheme='teal' |
|
||||||
variant='solid' |
|
||||||
_hover={{ textDecoration: 'none' }} |
|
||||||
> |
|
||||||
← |
|
||||||
<Text as='span' display={['none', 'inline']} ml='5px'> |
|
||||||
All Roadmaps |
|
||||||
</Text> |
|
||||||
</Button> |
|
||||||
|
|
||||||
<RoadmapDownloader roadmapTitle={roadmap.featuredTitle} /> |
|
||||||
<RoadmapSubscriber roadmapTitle={roadmap.featuredTitle} /> |
|
||||||
|
|
||||||
<Box flex={1} justifyContent='flex-end' display='flex'> |
|
||||||
<Button |
|
||||||
as={Link} |
|
||||||
href={`${siteConfig.url.issue}?title=[Suggestion] ${roadmap.title}`} |
|
||||||
target='_blank' |
|
||||||
size='xs' |
|
||||||
py='14px' |
|
||||||
px='10px' |
|
||||||
colorScheme='green' |
|
||||||
leftIcon={<ChatIcon />} |
|
||||||
_hover={{ textDecoration: 'none' }} |
|
||||||
> |
|
||||||
Suggest Changes |
|
||||||
</Button> |
|
||||||
</Box> |
|
||||||
</Stack> |
|
||||||
</Flex> |
|
||||||
{isInteractiveRoadmap(roadmap.id) && ( |
|
||||||
<Box mt='30px' mb={hasTNSAlert ? ['-53px', '-48px', '-63px'] : ['-37px', '-32px', '-47px']} borderWidth={1} rounded='3px'> |
|
||||||
{ hasTNSAlert && <TNSAlert roadmapName={roadmap.featuredTitle} />} |
|
||||||
<Text |
|
||||||
fontWeight={500} |
|
||||||
fontSize='14px' |
|
||||||
bg='white' |
|
||||||
p='5px 7px' |
|
||||||
rounded='3px' |
|
||||||
> |
|
||||||
<Badge pos='relative' top={'-1px'} mr='6px' colorScheme='yellow'> |
|
||||||
New |
|
||||||
</Badge> |
|
||||||
Resources are here, try clicking any nodes. |
|
||||||
</Text> |
|
||||||
</Box> |
|
||||||
)} |
|
||||||
</Container> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,52 +0,0 @@ |
|||||||
import { Box, Link, Text } from '@chakra-ui/react'; |
|
||||||
import { ExternalLinkIcon } from '@chakra-ui/icons'; |
|
||||||
import React from 'react'; |
|
||||||
import { event } from '../../lib/gtag'; |
|
||||||
|
|
||||||
type TNSAlertProps = { |
|
||||||
roadmapName: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export function TNSAlert(props: TNSAlertProps) { |
|
||||||
const { roadmapName } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Text |
|
||||||
fontWeight={500} |
|
||||||
fontSize='14px' |
|
||||||
bg='gray.100' |
|
||||||
p='5px 7px' |
|
||||||
rounded='2px 2px 0 0' |
|
||||||
borderBottomWidth={1} |
|
||||||
> |
|
||||||
<Box as='span' display={['none', 'none', 'inline']}>Get the latest {roadmapName} news from our sister site |
|
||||||
<Link |
|
||||||
href={'https://thenewstack.io?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Banner'} |
|
||||||
target='_blank' textDecoration='underline' |
|
||||||
onClick={() => { |
|
||||||
event({ |
|
||||||
category: 'PartnerClick', |
|
||||||
action: `TNS Referral`, |
|
||||||
label: `TNS Referral - ${roadmapName}`, |
|
||||||
}); |
|
||||||
}} |
|
||||||
fontWeight={600}>TheNewStack.io <ExternalLinkIcon /> |
|
||||||
</Link> |
|
||||||
</Box> |
|
||||||
<Box as='span' display={['inline', 'inline', 'none']}>Get latest {roadmapName} news on |
|
||||||
<Link |
|
||||||
href={'https://thenewstack.io?utm_source=roadmap-sh&utm_medium=Referral&utm_campaign=Banner'} |
|
||||||
target='_blank' textDecoration='underline' |
|
||||||
onClick={() => { |
|
||||||
event({ |
|
||||||
category: 'PartnerClick', |
|
||||||
action: `TNS Referral`, |
|
||||||
label: `TNS Referral - ${roadmapName}`, |
|
||||||
}); |
|
||||||
}} |
|
||||||
fontWeight={600}>TheNewStack.io <ExternalLinkIcon /> |
|
||||||
</Link> |
|
||||||
</Box> |
|
||||||
</Text> |
|
||||||
); |
|
||||||
} |
|
@ -1,43 +0,0 @@ |
|||||||
import { Box, Flex, Link } from '@chakra-ui/react'; |
|
||||||
import HackerNewsIcon from 'components/icons/hackernews-square.svg'; |
|
||||||
import FacebookIcon from 'components/icons/facebook-square.svg'; |
|
||||||
import TwitterIcon from 'components/icons/twitter-square.svg'; |
|
||||||
import RedditIcon from 'components/icons/reddit-square.svg'; |
|
||||||
import { Icon } from '@chakra-ui/icons'; |
|
||||||
import { getFacebookShareUrl, getHnShareUrl, getRedditShareUrl, getTwitterShareUrl } from '../lib/url'; |
|
||||||
import { useEffect, useState } from 'react'; |
|
||||||
|
|
||||||
type ShareIconProps = { |
|
||||||
text: string; |
|
||||||
url: string; |
|
||||||
} |
|
||||||
|
|
||||||
export function ShareIcons(props: ShareIconProps) { |
|
||||||
const { text, url } = props; |
|
||||||
|
|
||||||
const [offset, setOffset] = useState(0); |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
const onScroll = () => setOffset(window.scrollY); |
|
||||||
|
|
||||||
window.removeEventListener('scroll', onScroll); |
|
||||||
window.addEventListener('scroll', onScroll, { passive: true }); |
|
||||||
|
|
||||||
return () => window.removeEventListener('scroll', onScroll); |
|
||||||
}, []); |
|
||||||
|
|
||||||
if (offset <= 100) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box pos='absolute' left={'-15px'} top={'190px'} height='100%' display={['none', 'none', 'none', 'block']}> |
|
||||||
<Flex pos='sticky' top='100px' flexDir='column'> |
|
||||||
<Link target='_blank' color='gray.500' href={getTwitterShareUrl({ url, text })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={TwitterIcon} /></Link> |
|
||||||
<Link target='_blank' color='gray.500' href={getFacebookShareUrl({ url, text })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={FacebookIcon} /></Link> |
|
||||||
<Link target='_blank' color='gray.500' href={getHnShareUrl({ url, text })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={HackerNewsIcon} /></Link> |
|
||||||
<Link target='_blank' color='gray.500' href={getRedditShareUrl({ text, url })} _hover={{ color: "gray.700" }}><Icon fill='currentColor' height='24px' width='24px' as={RedditIcon} /></Link> |
|
||||||
</Flex> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,33 +0,0 @@ |
|||||||
import { Flex, Link, Text } from '@chakra-ui/react'; |
|
||||||
import YouTubeLogo from '../components/icons/youtube.svg'; |
|
||||||
import siteConfig from '../content/site.json'; |
|
||||||
import { event } from '../lib/gtag'; |
|
||||||
|
|
||||||
export function StickyBanner() { |
|
||||||
return ( |
|
||||||
<Flex as={Link} |
|
||||||
href={siteConfig.url.youtube} |
|
||||||
bg={'yellow.200'} |
|
||||||
color='gray.900' |
|
||||||
alignItems='center' |
|
||||||
position='sticky' |
|
||||||
top={0} |
|
||||||
zIndex={999} |
|
||||||
justifyContent='center' |
|
||||||
py='8px' |
|
||||||
_hover={{ textDecoration: 'none', bg: 'yellow.400' }} |
|
||||||
target='_blank' |
|
||||||
onClick={() => event({ |
|
||||||
category: 'Subscription', |
|
||||||
action: 'Clicked the YouTube banner', |
|
||||||
label: 'Sticky YouTube banner on Top' |
|
||||||
})} |
|
||||||
> |
|
||||||
<YouTubeLogo style={{ height: '20px', display: 'inline-block', marginRight: '7px' }} /> |
|
||||||
<Text as='span' fontWeight={500} fontSize='14px'> |
|
||||||
<Text as='span'>We now have a YouTube Channel. <Text as='span' d={['none', 'inline']}>Subscribe for the video |
|
||||||
content.</Text></Text> |
|
||||||
</Text> |
|
||||||
</Flex> |
|
||||||
); |
|
||||||
} |
|
@ -1,22 +0,0 @@ |
|||||||
import { Box, Button, Container, Heading, Link, Text } from '@chakra-ui/react'; |
|
||||||
import { event } from '../lib/gtag'; |
|
||||||
|
|
||||||
export function TeamsBanner() { |
|
||||||
return null; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box bg='teal.500' borderTopWidth={1} py={['45px', '45px', '70px']} textAlign='center'> |
|
||||||
<Container maxW='container.sm'> |
|
||||||
<Heading as='h4' color={'white'} fontSize={['25px', '25px', '35px']} mb={['10px', '10px', '20px']}>Roadmaps for Teams</Heading> |
|
||||||
<Text lineHeight='26px' color={'white'} fontSize={['15px', '15px', '18px']} mb='20px'>We are working on a solution for teams. Help us shape the platform!</Text> |
|
||||||
<Button onClick={() => { |
|
||||||
event({ |
|
||||||
category: 'UpcomingFeatureClick', |
|
||||||
action: `Teams Form Redirect`, |
|
||||||
label: `Click Teams Footer Link` |
|
||||||
}); |
|
||||||
}} target={'_blank'} as={Link} href='https://forms.gle/6X2matbCmjmvYGGt6' _hover={{textDecoration: 'none', bg: 'gray.300'}}>Take a Survey</Button> |
|
||||||
</Container> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,69 +0,0 @@ |
|||||||
import { Badge, Box, Heading, Link, Text } from '@chakra-ui/react'; |
|
||||||
|
|
||||||
type VideoGridItemProps = { |
|
||||||
href: string; |
|
||||||
title: string; |
|
||||||
subtitle: string; |
|
||||||
date: string; |
|
||||||
target?: string; |
|
||||||
isNew?: boolean; |
|
||||||
colorIndex?: number; |
|
||||||
}; |
|
||||||
|
|
||||||
const bgColorList = [ |
|
||||||
'gray.900', |
|
||||||
'purple.900', |
|
||||||
'blue.900', |
|
||||||
'red.900', |
|
||||||
'green.900', |
|
||||||
'teal.900', |
|
||||||
'yellow.900', |
|
||||||
'cyan.900', |
|
||||||
'pink.900', |
|
||||||
|
|
||||||
'gray.800', |
|
||||||
'purple.800', |
|
||||||
'blue.800', |
|
||||||
'red.800', |
|
||||||
'green.800', |
|
||||||
'teal.800', |
|
||||||
'yellow.800', |
|
||||||
'cyan.800', |
|
||||||
'pink.800', |
|
||||||
|
|
||||||
'gray.700', |
|
||||||
'purple.700', |
|
||||||
'blue.700', |
|
||||||
'red.700', |
|
||||||
'green.700', |
|
||||||
'teal.700', |
|
||||||
'yellow.700', |
|
||||||
'cyan.700', |
|
||||||
'pink.700', |
|
||||||
|
|
||||||
'gray.600', |
|
||||||
'purple.600', |
|
||||||
'blue.600', |
|
||||||
'red.600', |
|
||||||
'green.600', |
|
||||||
'teal.600', |
|
||||||
'yellow.600', |
|
||||||
'cyan.600', |
|
||||||
'pink.600' |
|
||||||
]; |
|
||||||
|
|
||||||
export function VideoGridItem(props: VideoGridItemProps) { |
|
||||||
const { title, subtitle, date, isNew = false, colorIndex = 0, href, target } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<Box _hover={{ textDecoration: 'none', transform: 'scale(1.02)' }} as={Link} href={ href } target={target || '_self'} shadow='xl' p='20px' |
|
||||||
rounded='10px' bg={bgColorList[colorIndex] ?? bgColorList[0]} flex={1}> |
|
||||||
<Text mb='7px' fontSize='12px' color='gray.400'> |
|
||||||
{isNew && <Badge colorScheme={'green'} mr='10px'>New</Badge>} |
|
||||||
{date} |
|
||||||
</Text> |
|
||||||
<Heading color='white' mb={'6px'} fontSize='20px' lineHeight={'28px'}>{title}</Heading> |
|
||||||
<Text color='gray.300' fontSize='14px'>{subtitle}</Text> |
|
||||||
</Box> |
|
||||||
); |
|
||||||
} |
|
@ -1,49 +0,0 @@ |
|||||||
[ |
|
||||||
{ |
|
||||||
"username": "kamranahmedse", |
|
||||||
"name": "Kamran Ahmed", |
|
||||||
"twitter": "kamranahmedse", |
|
||||||
"picture": "/authors/kamranahmedse.jpeg", |
|
||||||
"bio": "Lead engineer at Tajawal. Lover of all things web and opensource. Created roadmap.sh to help the confused ones." |
|
||||||
}, |
|
||||||
{ |
|
||||||
"username": "jesse", |
|
||||||
"name": "Jesse Li", |
|
||||||
"twitter": "__jesse_li", |
|
||||||
"picture": "/authors/jesse.png", |
|
||||||
"bio": "Software engineer." |
|
||||||
}, |
|
||||||
{ |
|
||||||
"username": "dmytrobol", |
|
||||||
"name": "Dmytro Bolkachov", |
|
||||||
"twitter": "dmytrobol", |
|
||||||
"picture": "/authors/dmytrobol.png", |
|
||||||
"bio": "JavaScript Lad, Movie buff and coder interested in everything web related" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"username": "spekulatius", |
|
||||||
"name": "Peter Thaleikis", |
|
||||||
"twitter": "spekulatius1984", |
|
||||||
"picture": "/authors/spekulatius.jpg", |
|
||||||
"bio": "Developer building side-projects for fun, lover of the web and open source" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"username": "ebrahimbharmal007", |
|
||||||
"name": "Ebrahim Bharmal", |
|
||||||
"twitter": "BharmalEbrahim", |
|
||||||
"picture": "/authors/ebrahimbharmal007.png", |
|
||||||
"bio": "Love building projects using tools completely new to me. Python forever. Senior at University of Texas at Arlington (2021)" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"username": "lesovsky", |
|
||||||
"name": "Alexey Lesovsky", |
|
||||||
"bio": "Linux system administrator and PostgreSQL DBA at DataEgret.", |
|
||||||
"picture": "/authors/lesovsky.jpeg" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"username": "danielgruesso", |
|
||||||
"name": "Daniel Gruesso", |
|
||||||
"bio": "Product manager working on blockchain and smart contracts developer tools", |
|
||||||
"picture": "/authors/danielgruesso.jpg" |
|
||||||
} |
|
||||||
] |
|
@ -1,314 +0,0 @@ |
|||||||
[ |
|
||||||
{ |
|
||||||
"id": "session-based-authentication", |
|
||||||
"title": "Session Based Authentication", |
|
||||||
"description": "Learn what is Session Based Authentication and how to implement it in Node.js", |
|
||||||
"isNew": true, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2022-11-01T19:59:14.191Z", |
|
||||||
"createdAt": "2022-11-01T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "http-basic-authentication", |
|
||||||
"title": "HTTP Basic Authentication", |
|
||||||
"description": "Learn what is HTTP Basic Authentication and how to implement it in Node.js", |
|
||||||
"isNew": true, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2022-10-03T19:59:14.191Z", |
|
||||||
"createdAt": "2022-10-03T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "basics-of-authentication", |
|
||||||
"title": "Basics of Authentication", |
|
||||||
"description": "Learn the basics of Authentication and Authorization", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2022-09-21T19:59:14.191Z", |
|
||||||
"createdAt": "2022-09-21T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "avoid-render-blocking-javascript-with-async-defer", |
|
||||||
"title": "Async and Defer Script Loading", |
|
||||||
"description": "Learn how to avoid render blocking JavaScript using async and defer scripts.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-09-10T19:59:14.191Z", |
|
||||||
"createdAt": "2021-09-10T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "what-are-web-vitals", |
|
||||||
"title": "What are Web Vitals?", |
|
||||||
"description": "Learn what are the core web vitals and how to measure them.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-09-05T19:59:14.191Z", |
|
||||||
"createdAt": "2021-09-05T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "what-is-sli-slo-sla", |
|
||||||
"title": "SLIs, SLOs and SLAs", |
|
||||||
"description": "Learn what are different indicators for performance identification of any service.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-08-31T19:59:14.191Z", |
|
||||||
"createdAt": "2021-08-31T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "ci-cd", |
|
||||||
"title": "What is CI and CD?", |
|
||||||
"description": "Learn the basics of CI/CD and how to implement that with GitHub Actions.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-07-09T19:59:14.191Z", |
|
||||||
"createdAt": "2021-07-09T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "sso", |
|
||||||
"title": "SSO — Single Sign On", |
|
||||||
"description": "Learn the basics of SAML and understand how does Single Sign On work.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-07-01T19:59:14.191Z", |
|
||||||
"createdAt": "2021-07-01T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "oauth", |
|
||||||
"title": "OAuth — Open Authorization", |
|
||||||
"description": "Learn and understand what is OAuth and how it works", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-06-28T19:59:14.191Z", |
|
||||||
"createdAt": "2021-06-28T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "jwt-authentication", |
|
||||||
"title": "JWT Authentication", |
|
||||||
"description": "Understand what is JWT authentication and how is it implemented", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-06-20T19:59:14.191Z", |
|
||||||
"createdAt": "2021-06-20T19:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "token-authentication", |
|
||||||
"title": "Token Based Authentication", |
|
||||||
"description": "Understand what is token based authentication and how it is implemented", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-06-02T20:59:14.191Z", |
|
||||||
"createdAt": "2021-06-02T20:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "session-authentication", |
|
||||||
"title": "Session Based Authentication", |
|
||||||
"description": "Understand what is session based authentication and how it is implemented", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-05-26T20:59:14.191Z", |
|
||||||
"createdAt": "2021-05-26T20:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "basic-authentication", |
|
||||||
"title": "Basic Authentication", |
|
||||||
"description": "Understand what is basic authentication and how it is implemented", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-05-19T20:59:14.191Z", |
|
||||||
"createdAt": "2021-05-19T20:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "character-encodings", |
|
||||||
"title": "Character Encodings", |
|
||||||
"description": "Covers the basics of character encodings and explains ASCII vs Unicode", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-05-14T20:59:14.191Z", |
|
||||||
"createdAt": "2021-05-14T20:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "unfamiliar-codebase", |
|
||||||
"title": "Unfamiliar Codebase", |
|
||||||
"description": "Tips on getting familiar with an unfamiliar codebase", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-05-04T20:59:14.191Z", |
|
||||||
"createdAt": "2021-05-04T20:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "why-build-it-and-they-will-come-wont-work-anymore", |
|
||||||
"title": "Build it and they will come?", |
|
||||||
"description": "Why “build it and they will come” alone won’t work anymore", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "spekulatius", |
|
||||||
"updatedAt": "2021-05-04T12:59:14.191Z", |
|
||||||
"createdAt": "2021-05-04T12:59:14.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "dhcp-in-one-picture", |
|
||||||
"title": "DHCP in One Picture", |
|
||||||
"description": "Here is what happens when a new device joins the network.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-04-28T15:48:21.191Z", |
|
||||||
"createdAt": "2021-04-28T15:48:21.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "ssl-tls-https-ssh", |
|
||||||
"title": "SSL vs TLS vs SSH", |
|
||||||
"description": "Quick tidbit on the differences between SSL, TLS, HTTPS and SSH", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-04-22T15:48:21.191Z", |
|
||||||
"createdAt": "2021-04-22T15:48:21.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "asymptotic-notation", |
|
||||||
"title": "Asymptotic Notation", |
|
||||||
"description": "Learn the basics of measuring the time and space complexity of algorithms", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-04-03T15:48:21.191Z", |
|
||||||
"createdAt": "2021-04-03T15:48:21.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "big-o-notation", |
|
||||||
"title": "Big-O Notation", |
|
||||||
"description": "Easy to understand explanation of Big-O notation without any fancy terms", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-03-15T15:48:21.191Z", |
|
||||||
"createdAt": "2021-03-15T15:48:21.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "random-numbers", |
|
||||||
"title": "Random Numbers: Are they?", |
|
||||||
"description": "Learn how they are generated and why they may not be truly random.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-03-14T15:48:21.191Z", |
|
||||||
"createdAt": "2021-03-14T15:48:21.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "scaling-databases", |
|
||||||
"title": "Scaling Databases", |
|
||||||
"description": "Learn the ups and downs of different database scaling strategies", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-02-18T15:48:21.191Z", |
|
||||||
"createdAt": "2021-02-18T15:48:21.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "what-is-internet", |
|
||||||
"title": "How does the internet work?", |
|
||||||
"description": "Learn the basics of internet and everything involved with this short video series", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2021-02-29T15:48:21.191Z", |
|
||||||
"createdAt": "2021-02-29T15:48:21.191Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "torrent-client", |
|
||||||
"title": "Building a BitTorrent Client", |
|
||||||
"description": "Learn everything you need to know about BitTorrent by writing a client in Go", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "jesse", |
|
||||||
"updatedAt": "2021-01-17T15:48:21.191Z", |
|
||||||
"createdAt": "2021-01-17T15:48:21.191Z", |
|
||||||
"canonical": "https://blog.jse.li/posts/torrent/" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "levels-of-seniority", |
|
||||||
"title": "Levels of Seniority", |
|
||||||
"description": "How to Step Up as a Junior, Mid Level or a Senior Developer?", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2020-12-03T12:13:00.860Z", |
|
||||||
"createdAt": "2020-12-03T12:13:00.860Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "design-patterns-for-humans", |
|
||||||
"title": "Design Patterns for Humans", |
|
||||||
"description": "A language agnostic, ultra-simplified explanation to design patterns", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2019-10-09T12:00:00.860Z", |
|
||||||
"createdAt": "2019-01-23T17:00:00.860Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "journey-to-http2", |
|
||||||
"title": "Journey to HTTP/2", |
|
||||||
"description": "The evolution of HTTP. How it all started and where we stand today", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"createdAt": "2018-12-04T12:00:00.860Z", |
|
||||||
"updatedAt": "2018-12-04T12:00:00.860Z", |
|
||||||
"isDraft": true |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "dns-in-one-picture", |
|
||||||
"title": "DNS in One Picture", |
|
||||||
"description": "Quick illustrative guide on how a website is found on the internet.", |
|
||||||
"isNew": false, |
|
||||||
"type": "visual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"updatedAt": "2018-12-04T12:00:00.860Z", |
|
||||||
"createdAt": "2018-12-04T17:00:00.860Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "http-caching", |
|
||||||
"title": "HTTP Caching", |
|
||||||
"description": "Everything you need to know about web caching", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"createdAt": "2018-11-29T17:00:00.860Z", |
|
||||||
"updatedAt": "2018-11-29T17:00:00.860Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "history-of-javascript", |
|
||||||
"title": "Brief History of JavaScript", |
|
||||||
"description": "How JavaScript was introduced and evolved over the years", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "kamranahmedse", |
|
||||||
"createdAt": "2017-10-28T17:00:00.860Z", |
|
||||||
"updatedAt": "2017-10-28T17:00:00.860Z" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"id": "proxy-servers", |
|
||||||
"title": "Proxy Servers", |
|
||||||
"description": "How do proxy servers work and what are forward and reverse proxies?", |
|
||||||
"isNew": false, |
|
||||||
"type": "textual", |
|
||||||
"authorUsername": "ebrahimbharmal007", |
|
||||||
"createdAt": "2017-10-24T17:00:00.860Z", |
|
||||||
"updatedAt": "2017-10-24T17:00:00.860Z" |
|
||||||
} |
|
||||||
] |
|
@ -1,4 +0,0 @@ |
|||||||
Asymptotic notation is the standard way of measuring the time and space that an algorithm will consume as the input grows. In one of my last guides, I covered "Big-O notation" and a lot of you asked for a similar one for Asymptotic notation. You can find the [previous guide here](/guides/big-o-notation). |
|
||||||
|
|
||||||
[![](/guides/asymptotic-notation.png)](/guides/asymptotic-notation.png) |
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/avoid-render-blocking-javascript-with-async-defer.png)](/guides/avoid-render-blocking-javascript-with-async-defer.png) |
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/basic-authentication.png)](/guides/basic-authentication.png) |
|
||||||
|
|
@ -1,83 +0,0 @@ |
|||||||
Our last video series was about data structures. We looked at the most common data structures, their use cases, pros and cons, and the different operations you could perform on each data structure. |
|
||||||
|
|
||||||
Today, we are kicking off a similar series for Authentication strategies where we will discuss everything you need to know about authentication and authentication strategies. |
|
||||||
|
|
||||||
In this guide today will be talking about what authentication is, and we will cover some terminology that will help us later in the series. You can watch the video below or continue reading this guide. |
|
||||||
|
|
||||||
<iframe src="https://www.youtube.com/embed/Mcyt9SrZT6g" title="Basics of Authentication" /> |
|
||||||
|
|
||||||
## What is Authentication? |
|
||||||
|
|
||||||
Authentication is the process of verifying someone's identity. A real-world example of that would be when you board a plane, the airline worker checks your passport to verify your identity, so the airport worker authenticates you. |
|
||||||
|
|
||||||
If we talk about computers, when you log in to any website, you usually authenticate yourself by entering your username and password, which is then checked by the website to ensure that you are who you claim to be. There are two things you should keep in mind: |
|
||||||
|
|
||||||
- Authentication is not only for the persons |
|
||||||
- And username and password are not the only way to authenticate. |
|
||||||
|
|
||||||
Some other examples are: |
|
||||||
|
|
||||||
- When you open a website in the browser. If the website uses HTTP, TLS is used to authenticate the server and avoid the fake loading of websites. |
|
||||||
|
|
||||||
- There might be server-to-server communication on the website. The server may need to authenticate the incoming request to avoid malicious usage. |
|
||||||
|
|
||||||
## How does Authentication Work? |
|
||||||
|
|
||||||
On a high level, we have the following factors used for authentication. |
|
||||||
|
|
||||||
- **Username and Password** |
|
||||||
- **Security Codes, Pin Codes, or Security Questions** — An example would be the pin code you enter at an ATM to withdraw cash. |
|
||||||
- **Hard Tokens and Soft Tokens** — Hard tokens are the special hardware devices that you attach to your device to authenticate yourself. Soft tokens, unlike hard tokens, don't have any authentication-specific device; we must verify the possession of a device that was used to set up the identity. For example, you may receive an OTP to log in to your account on a website. |
|
||||||
- **Biometric Authentication** — In biometric authentication, we authenticate using biometrics such as iris, facial, or voice recognition. |
|
||||||
|
|
||||||
We can categorize the factors above into three different types. |
|
||||||
|
|
||||||
- Username / Password and Security codes rely on the person's knowledge: we can group them under the **Knowledge Factor**. |
|
||||||
|
|
||||||
- In hard and soft tokens, we authenticate by checking the possession of hardware, so this would be a **Possession Factor**. |
|
||||||
|
|
||||||
- And in biometrics, we test the person's inherent qualities, i.e., iris, face, or voice, so this would be a **Qualities** factor. |
|
||||||
|
|
||||||
This brings us to our next topic: Multi-factor Authentication and Two-Factor Authentication. |
|
||||||
|
|
||||||
## Multifactor Authentication |
|
||||||
|
|
||||||
Multifactor authentication is the type of authentication in which we rely on more than one factor to authenticate a user. |
|
||||||
|
|
||||||
For example, if we pick up username/password from the **knowledge factor**. And we pick soft tokens from the **possession factor**, and we say that for a user to authenticate, they must enter their credentials and an OTP, which will be sent to their mobile phone, so this would be an example of multifactor authentication. |
|
||||||
|
|
||||||
In multifactor authentication, since we rely on more than one factor, this way of authentication is much more secure than single-factor authentication. |
|
||||||
|
|
||||||
One important thing to note here is that the factors you pick for authentication, they must differ. So, for example, if we pick up a username/password and security question or security codes, it is still not true multifactor authentication because we still rely on the knowledge factor. The factors have to be different from each other. |
|
||||||
|
|
||||||
### Two-Factor Authentication |
|
||||||
|
|
||||||
Two-factor authentication is similar to multifactor authentication. The only difference is that there are precisely two factors in 2FA. In MFA, we can have 2, 3, 4, or any authentication factors; 2FA has exactly two factors. We can say that 2FA is always MFA, because there are more than one factors. MFA is not always 2FA because there may be more than two factors involved. |
|
||||||
|
|
||||||
Next we have the difference between authentication and authorization. This comes up a lot in the interviews, and beginners often confuse them. |
|
||||||
|
|
||||||
### What is Authentication |
|
||||||
Authentication is the process of verifying the identity. For example, when you enter your credentials at a login screen, the application here identifies you through your credentials. So this is what the authentication is, the process of verifying the identity. |
|
||||||
|
|
||||||
In case of an authentication failure, for example, if you enter an invalid username and password, the HTTP response code is "Unauthorized" 401. |
|
||||||
|
|
||||||
### What is Authorization |
|
||||||
|
|
||||||
Authorization is the process of checking permission. Once the user has logged in, i.e., the user has been authenticated, the process of reviewing the permission to see if the user can perform the relevant operation or not is called authorization. |
|
||||||
|
|
||||||
And in case of authorization failure, i.e., if the user tries to perform an operation they are not allowed to perform, the HTTP response code is forbidden 403. |
|
||||||
|
|
||||||
## Authentication Strategies |
|
||||||
|
|
||||||
Given below is the list of common authentication strategies: |
|
||||||
|
|
||||||
- Basics of Authentication |
|
||||||
- Session Based Authentication |
|
||||||
- Token-Based Authentication |
|
||||||
- JWT Authentication |
|
||||||
- OAuth - Open Authorization |
|
||||||
- Single Sign On (SSO) |
|
||||||
|
|
||||||
In this series of illustrated videos and textual guides, we will be going through each of the strategies discussing what they are, how they are implemented, the pros and cons and so on. |
|
||||||
|
|
||||||
So stay tuned, and I will see you in the next one. |
|
@ -1,4 +0,0 @@ |
|||||||
Big-O notation is the mathematical notation that helps analyse the algorithms to get an idea about how they might perform as the input grows. The image below explains Big-O in a simple way without using any fancy terminology. |
|
||||||
|
|
||||||
[![](/guides/big-o-notation.png)](/guides/big-o-notation.png) |
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/character-encodings.png)](/guides/character-encodings.png) |
|
||||||
|
|
@ -1,4 +0,0 @@ |
|||||||
The image below details the differences between the continuous integration and continuous delivery. Also, here is the [accompanying video on implementing that with GitHub actions](https://www.youtube.com/watch?v=nyKZTKQS_EQ). |
|
||||||
|
|
||||||
[![](/guides/ci-cd.png)](/guides/ci-cd.png) |
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/dhcp.png)](/guides/dhcp.png) |
|
||||||
|
|
@ -1,5 +0,0 @@ |
|||||||
DNS or Domain Name System is one of the fundamental blocks of the internet. As a developer, you should have at-least the basic understanding of how it works. This article is a brief introduction to what is DNS and how it works. |
|
||||||
|
|
||||||
DNS at its simplest is like a phonebook on your mobile phone. Whenever you have to call one of your contacts, you can either dial their number from your memory or use their name which will then be used by your mobile phone to search their number in your phone book to call them. Every time you make a new friend, or your existing friend gets a mobile phone, you have to memorize their phone number or save it in your phonebook to be able to call them later on. DNS or Domain Name System, in a similar fashion, is a mechanism that allows you to browse websites on the internet. Just like your mobile phone does not know how to call without knowing the phone number, your browser does not know how to open a website just by the domain name; it needs to know the IP Address for the website to open. You can either type the IP Address to open, or provide the domain name and press enter which will then be used by your browser to find the IP address by going through several hoops. The picture below is the illustration of how your browser finds a website on the internet. |
|
||||||
|
|
||||||
[![](https://i.imgur.com/z9rwm5A.png)](https://i.imgur.com/z9rwm5A.png) |
|
@ -1,41 +0,0 @@ |
|||||||
Around 10 years ago, Jeff Atwood (the founder of stackoverflow) made a case that JavaScript is going to be the future and he coined the “Atwood Law” which states that *Any application that can be written in JavaScript will eventually be written in JavaScript*. Fast-forward to today, 10 years later, if you look at it it rings truer than ever. JavaScript is continuing to gain more and more adoption. |
|
||||||
|
|
||||||
### JavaScript is announced |
|
||||||
JavaScript was initially created by [Brendan Eich](https://twitter.com/BrendanEich) of NetScape and was first announced in a press release by Netscape in 1995. It has a bizarre history of naming; initially it was named `Mocha` by the creator, which was later renamed to `LiveScript`. In 1996, about a year later after the release, NetScape decided to rename it to be `JavaScript` with hopes of capitalizing on the Java community (although JavaScript did not have any relationship with Java) and released Netscape 2.0 with the official support of JavaScript. |
|
||||||
|
|
||||||
### ES1, ES2 and ES3 |
|
||||||
In 1996, Netscape decided to submit it to [ECMA International](https://en.wikipedia.org/wiki/Ecma_International) with the hopes of getting it standardized. First edition of the standard specification was released in 1997 and the language was standardized. After the initial release, `ECMAScript` was continued to be worked upon and in no-time two more versions were released ECMAScript 2 in 1998 and ECMAScript 3 in 1999. |
|
||||||
|
|
||||||
### Decade of Silence and ES4 |
|
||||||
After the release of ES3 in 1999, there was a complete silence for a decade and no changes were made to the official standard. There was some work on the fourth edition in the initial days; some of the features that were being discussed included classes, modules, static typings, destructuring etc. It was being targeted to be released by 2008 but was abandoned due to political differences concerning language complexity. However, the vendors kept introducing the extensions to the language and the developers were left scratching their heads — adding polyfills to battle compatibility issues between different browsers. |
|
||||||
|
|
||||||
### From silence to ES5 |
|
||||||
Google, Microsoft, Yahoo and other disputers of ES4 came together and decided to work on a less ambitious update to ES3 tentatively named ES3.1. But the teams were still fighting about what to include from ES4 and what not. Finally, in 2009 ES5 was released mainly focusing on fixing the compatibility and security issues etc. But there wasn’t much of a splash in the water — it took ages for the vendors to incorporate the standards and many developers were still using ES3 without being aware of the “modern” standards. |
|
||||||
|
|
||||||
### Release of ES6 — ECMAScript 2015 |
|
||||||
After a few years of the release of ES5, things started to change, TC39 (the committee under ECMA international responsible for ECMAScript standardization) kept working on the next version of ECMAScript (ES6) which was originally named ES Harmony, before being eventually released with the name ES2015. ES2015 adds significant features and syntactic sugar to allow writing complex applications. Some of the features that ES6 has to offer, include Classes, Modules, Arrows, Enhanced object literals, Template strings, Destructuring, Default param values + rest + spread, Let and Const, Iterators + for..of, Generators, Maps + Sets, Proxies, Symbols, Promises, math + number + string + array + object APIs [etc](http://es6-features.org/#Constants) |
|
||||||
|
|
||||||
Browser support for ES6 is still scarce but everything that ES6 has to offer is still available to developers by transpiling the ES6 code to ES5. With the release of 6th version of ECMAScript, TC39 decided to move to yearly model of releasing updates to ECMAScript so to make sure that the new features are added as soon as they are approved and we don’t have to wait for the full specification to be drafted and approved — thus 6th version of ECMAScript was renamed as ECMAScript 2015 or ES2015 before the release in June 2015. And the next versions of ECMAScript were decided to published in June of every year. |
|
||||||
|
|
||||||
### Release of ES7 — ECMAScript 2016 |
|
||||||
In June 2016, seventh version of ECMAScript was released. As ECMAScript has been moved to an yearly release model, ECMAScript 2016 (ES2016) comparatively did not have much to offer. ES2016 includes just two new features |
|
||||||
|
|
||||||
* Exponentiation operator `**` |
|
||||||
* `Array.prototype.includes` |
|
||||||
|
|
||||||
### Release of ES8 — ECMAScript 2017 |
|
||||||
The eighth version of ECMAScript was released in June 2017. The key highlight of ES8 was the addition of async functions. Here is the list of new features in ES8 |
|
||||||
|
|
||||||
* `Object.values()` and `Object.entries()` |
|
||||||
* String padding i.e. `String.prototype.padEnd()` and `String.prototype.padStart()` |
|
||||||
* `Object.getOwnPropertyDescriptors` |
|
||||||
* Trailing commas in function parameter lists and calls |
|
||||||
* Async functions |
|
||||||
|
|
||||||
### What is ESNext then? |
|
||||||
ESNext is a dynamic name that refers to whatever the current version of ECMAScript is at the given time. For example, at the time of this writing `ES2017` or `ES8` is `ESNext`. |
|
||||||
|
|
||||||
### What does the future hold? |
|
||||||
Since the release of ES6, [TC39](https://github.com/tc39) has quite streamlined their process. TC39 operates through a Github organization now and there are [several proposals](https://github.com/tc39/proposals) for new features or syntax to be added to the next versions of ECMAScript. Any one can go ahead and [submit a proposal](https://github.com/tc39/proposals) thus resulting in increasing the participation from the community. Every proposal goes through [four stages of maturity](https://tc39.github.io/process-document/) before it makes it into the specification. |
|
||||||
|
|
||||||
And that about wraps it up. Feel free to leave your feedback in the comments section below. Also here are the links to original language specifications [ES6](https://www.ecma-international.org/ecma-262/6.0/), [ES7](https://www.ecma-international.org/ecma-262/7.0/) and [ES8](https://www.ecma-international.org/ecma-262/8.0/). |
|
@ -1,93 +0,0 @@ |
|||||||
Our last guide was about the [basics of authentication](/guides/basics-of-authentication), where we discussed authentication, authorization, types of authentication, authentication factors, authentication strategies, and so on. |
|
||||||
|
|
||||||
In this guide today, we will be learning about basic authentication, and we will see how we can implement Basic Authentication in Node.js. We have a [visual guide on the basic authentication](/guides/basic-authentication) and an illustrative video, watch the video below or continue reading: |
|
||||||
|
|
||||||
<iframe src="https://www.youtube.com/embed/mwccHwUn7Gc" title="HTTP Basic Authentication" /> |
|
||||||
|
|
||||||
## What is Basic Authentication? |
|
||||||
Given the name "Basic Authentication", you should not confuse Basic Authentication with the standard username and password authentication. Basic authentication is a part of the HTTP specification, and the details can be [found in the RFC7617](https://www.rfc-editor.org/rfc/rfc7617.html). |
|
||||||
|
|
||||||
Because it is a part of the HTTP specifications, all the browsers have native support for "HTTP Basic Authentication". Given below is the screenshot from the implementation in Google Chrome. |
|
||||||
|
|
||||||
![](/guides/basic-authentication/chrome-basic-auth.png) |
|
||||||
|
|
||||||
|
|
||||||
## How does it Work? |
|
||||||
Now that we know what basic authentication is, the question is, how does it work? The answer is: it is controlled by the response of the server. |
|
||||||
|
|
||||||
### Step 1 |
|
||||||
When the browser first requests the server, the server tries to check the availability of the `Authorization` header in the request. Because it is the first request, no `Authorization` header is found in the request. So the server responds with the `401 Unauthorized` response code and also sends the `WWW-Authenticate` header with the value set to `Basic`, which tells the browser that it needs to trigger the basic authentication flow. |
|
||||||
|
|
||||||
``` |
|
||||||
401 Unauthorized |
|
||||||
WWW-Authenticate: Basic realm='user_pages' |
|
||||||
``` |
|
||||||
|
|
||||||
If you notice the response, we have an additional parameter called `realm`, which is just a value assigned to a group of pages that share the same credentials. |
|
||||||
|
|
||||||
The browser might use Realm to cache the credential. In the future, when there is an authentication failure browser will check if it has the credentials in the cache for the given realm of the domain, and it may use the same credentials. |
|
||||||
|
|
||||||
## Step 2 |
|
||||||
Upon receiving the response from the server, the browser will notice the `WWW-Authenticate` header and will show the authentication popup. |
|
||||||
|
|
||||||
![](/guides/basic-authentication/chrome-basic-auth.png) |
|
||||||
|
|
||||||
## Step 3 |
|
||||||
After the user submits the credentials through this authentication popup, the browser will automatically encode the credentials using the `base64` encoding and send them in the `Authorization` header of the same request. |
|
||||||
|
|
||||||
### Step 4 |
|
||||||
Upon receiving the request, the server will decode and verify the credentials. If the credentials are valid, the server will send the response to the client. |
|
||||||
|
|
||||||
So that is how Basic Authentication works. |
|
||||||
|
|
||||||
## Basic Authentication in Node.js |
|
||||||
I have prepared the sample project in Node.js, which can be found on GitHub [kamranahmedse/node-basic-auth-example](https://github.com/kamranahmedse/node-basic-auth-example). If you look at the codebase of the project, we have two files `index.js` with the following content: |
|
||||||
|
|
||||||
```javascript |
|
||||||
// src/index.js |
|
||||||
|
|
||||||
const express = require('express'); |
|
||||||
const authMiddleware = require('./auth'); |
|
||||||
|
|
||||||
const app = express(); |
|
||||||
const port = 3000; |
|
||||||
|
|
||||||
// This middleware is where we have the |
|
||||||
// basic authentication implementation |
|
||||||
app.use(authMiddleware); |
|
||||||
|
|
||||||
app.get('/', (req, res) => { |
|
||||||
res.send('Hello World!'); |
|
||||||
}); |
|
||||||
|
|
||||||
app.listen(port, () => { |
|
||||||
console.log(`App running @ http://localhost:${port}`); |
|
||||||
}) |
|
||||||
``` |
|
||||||
|
|
||||||
As you can see, it's just a regular express server. `authMiddleware` registration is where we have all the code for "Basic Authentication". Here is the content of the middleware: |
|
||||||
|
|
||||||
```javascript |
|
||||||
// src/auth.js |
|
||||||
const base64 = require("base-64"); |
|
||||||
|
|
||||||
function decodeCredentials(authHeader) { |
|
||||||
// ... |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = function(req, res, next) { |
|
||||||
// Take the header and decode credentials |
|
||||||
const [username, password] = decodeCredentials(req.headers.authorization || ''); |
|
||||||
|
|
||||||
// Verify the credentials |
|
||||||
if (username === 'admin' && password === 'admin') { |
|
||||||
return next(); |
|
||||||
} |
|
||||||
|
|
||||||
// Respond with authenticate header on auth failure. |
|
||||||
res.set('WWW-Authenticate', 'Basic realm="user_pages"'); |
|
||||||
res.status(401).send('Authentication required.'); |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
And that is how the basic authentication is implemented in Node.js. |
|
@ -1,251 +0,0 @@ |
|||||||
As users, we easily get frustrated by the buffering of videos, the images that take seconds to load, and pages that got stuck because the content is being loaded. Loading the resources from some cache is much faster than fetching the same from the originating server. It reduces latency, speeds up the loading of resources, decreases the load on the server, cuts down the bandwidth costs etc. |
|
||||||
|
|
||||||
### Introduction |
|
||||||
|
|
||||||
What is a web cache? It is something that sits somewhere between the client and the server, continuously looking at the requests and their responses, looking for any responses that can be cached. So that there is less time consumed when the same request is made again. |
|
||||||
|
|
||||||
![Web Cache](https://i.imgur.com/mJYVvTh.png) |
|
||||||
|
|
||||||
> Note that this image is just to give you an idea. Depending upon the type of cache, the place where it is implemented could vary. More on this later. |
|
||||||
|
|
||||||
Before we get into further details, let me give you an overview of the terms that will be used, further in the article |
|
||||||
|
|
||||||
- **Client** could be your browser or any application requesting the server for some resource |
|
||||||
- **Origin Server**, the source of truth, houses all the content required by the client and is responsible for fulfilling the client's requests. |
|
||||||
- **Stale Content** is cached but expired content |
|
||||||
- **Fresh Content** is the content available in the cache that hasn't expired yet |
|
||||||
- **Cache Validation** is the process of contacting the server to check the validity of the cached content and get it updated for when it is going to expire |
|
||||||
- **Cache Invalidation** is the process of removing any stale content available in the cache |
|
||||||
|
|
||||||
![](https://i.imgur.com/9MjlzvW.png) |
|
||||||
|
|
||||||
### Caching Locations |
|
||||||
|
|
||||||
Web cache can be shared or private depending upon the location where it exists. Here is the list of different caching locations |
|
||||||
|
|
||||||
- [Browser Cache](#browser-cache) |
|
||||||
- [Proxy Cache](#proxy-cache) |
|
||||||
- [Reverse Proxy Cache](#reverse-proxy-cache) |
|
||||||
|
|
||||||
#### Browser Cache |
|
||||||
|
|
||||||
You might have noticed that when you click the back button in your browser it takes less time to load the page than the time that it took during the first load; this is the browser cache in play. Browser cache is the most common location for caching and browsers usually reserve some space for it. |
|
||||||
|
|
||||||
![Web Cache](https://i.imgur.com/mJYVvTh.png) |
|
||||||
|
|
||||||
A browser cache is limited to just one user and unlike other caches, it can store the "private" responses. More on it later. |
|
||||||
|
|
||||||
#### Proxy Cache |
|
||||||
|
|
||||||
Unlike browser cache which serves a single user, proxy caches may serve hundreds of different users accessing the same content. They are usually implemented on a broader level by ISPs or any other independent entities for example. |
|
||||||
|
|
||||||
![Proxy Cache](https://i.imgur.com/3mj6e1O.png) |
|
||||||
|
|
||||||
#### Reverse Proxy Cache |
|
||||||
|
|
||||||
A Reverse proxy cache or surrogate cache is implemented close to the origin servers in order to reduce the load on the server. Unlike proxy caches which are implemented by ISPs etc to reduce the bandwidth usage in a network, surrogates or reverse proxy caches are implemented near the origin servers by the server administrators to reduce the load on the server. |
|
||||||
|
|
||||||
![Reverse Proxy Cache](http://i.imgur.com/Eg4Cru3.png) |
|
||||||
|
|
||||||
Although you can control the reverse proxy caches (since it is implemented by you on your server) you can not avoid or control browser and proxy caches. And if your website is not configured to use these caches properly, it will still be cached using whatever defaults are set on these caches. |
|
||||||
|
|
||||||
### Caching Headers |
|
||||||
|
|
||||||
So, how do we control the web cache? Whenever the server emits some response, it is accompanied by some HTTP headers to guide the caches on whether and how to cache this response. The content provider is the one that has to make sure to return proper HTTP headers to force the caches on how to cache the content. |
|
||||||
|
|
||||||
- [Expires](#expires) |
|
||||||
- [Pragma](#pragma) |
|
||||||
- [Cache-Control](#cache-control) |
|
||||||
- [private](#private) |
|
||||||
- [public](#public) |
|
||||||
- [no-store](#no-store) |
|
||||||
- [no-cache](#no-cache) |
|
||||||
- [max-age: seconds](#max-age) |
|
||||||
- [s-maxage: seconds](#s-maxage) |
|
||||||
- [must-revalidate](#must-revalidate) |
|
||||||
- [proxy-revalidate](#proxy-revalidate) |
|
||||||
- [Mixing Values](#mixing-values) |
|
||||||
- [Validators](#validators) |
|
||||||
- [ETag](#etag) |
|
||||||
- [Last-Modified](#last-modified) |
|
||||||
|
|
||||||
#### Expires |
|
||||||
|
|
||||||
Before HTTP/1.1 and the introduction of `Cache-Control`, there was an `Expires` header which is simply a timestamp telling the caches how long should some content be considered fresh. A possible value to this header is the absolute expiry date; where a date has to be in GMT. Below is the sample header |
|
||||||
|
|
||||||
```html |
|
||||||
Expires: Mon, 13 Mar 2017 12:22:00 GMT |
|
||||||
``` |
|
||||||
|
|
||||||
It should be noted that the date cannot be more than a year and if the date format is wrong, the content will be considered stale. Also, the clock on the cache has to be in sync with the clock on the server, otherwise, the desired results might not be achieved. |
|
||||||
|
|
||||||
Although the `Expires` header is still valid and is supported widely by the caches, preference should be given to HTTP/1.1 successor of it i.e. `Cache-Control`. |
|
||||||
|
|
||||||
#### Pragma |
|
||||||
|
|
||||||
Another one from the old, pre HTTP/1.1 days, is `Pragma`. Everything that it could do is now possible using the cache-control header given below. However, one thing I would like to point out about it is, that you might see `Pragma: no-cache` being used here and there in hopes of stopping the response from being cached. It might not necessarily work; as HTTP specification discusses it in the request headers and there is no mention of it in the response headers. Rather `Cache-Control` header should be used to control the caching. |
|
||||||
|
|
||||||
#### Cache-Control |
|
||||||
|
|
||||||
Cache-Control specifies how long and in what manner should the content be cached. This family of headers was introduced in HTTP/1.1 to overcome the limitations of the `Expires` header. |
|
||||||
|
|
||||||
Value for the `Cache-Control` header is composite i.e. it can have multiple directive/values. Let's look at the possible values that this header may contain. |
|
||||||
|
|
||||||
##### private |
|
||||||
Setting the cache to `private` means that the content will not be cached in any of the proxies and it will only be cached by the client (i.e. browser) |
|
||||||
|
|
||||||
```html |
|
||||||
Cache-Control: private |
|
||||||
``` |
|
||||||
|
|
||||||
Having said that, don't let it fool you into thinking that setting this header will make your data any secure; you still have to use SSL for that purpose. |
|
||||||
|
|
||||||
##### public |
|
||||||
|
|
||||||
If set to `public`, apart from being cached by the client, it can also be cached by the proxies; serving many other users |
|
||||||
|
|
||||||
```html |
|
||||||
Cache-Control: public |
|
||||||
``` |
|
||||||
|
|
||||||
##### no-store |
|
||||||
**`no-store`** specifies that the content is not to be cached by any of the caches |
|
||||||
|
|
||||||
```html |
|
||||||
Cache-Control: no-store |
|
||||||
``` |
|
||||||
|
|
||||||
##### no-cache |
|
||||||
**`no-cache`** indicates that the cache can be maintained but the cached content is to be re-validated (using `ETag` for example) from the server before being served. That is, there is still a request to server but for validation and not to download the cached content. |
|
||||||
|
|
||||||
```html |
|
||||||
Cache-Control: max-age=3600, no-cache, public |
|
||||||
``` |
|
||||||
|
|
||||||
##### max-age: seconds |
|
||||||
**`max-age`** specifies the number of seconds for which the content will be cached. For example, if the `cache-control` looks like below: |
|
||||||
|
|
||||||
```html |
|
||||||
Cache-Control: max-age=3600, public |
|
||||||
``` |
|
||||||
it would mean that the content is publicly cacheable and will be considered stale after 60 minutes |
|
||||||
|
|
||||||
##### s-maxage: seconds |
|
||||||
**`s-maxage`** here `s-` prefix stands for shared. This directive specifically targets the shared caches. Like `max-age` it also gets the number of seconds for which something is to be cached. If present, it will override `max-age` and `expires` headers for shared caching. |
|
||||||
|
|
||||||
```html |
|
||||||
Cache-Control: s-maxage=3600, public |
|
||||||
``` |
|
||||||
|
|
||||||
##### must-revalidate |
|
||||||
**`must-revalidate`** it might happen sometimes that if you have network problems and the content cannot be retrieved from the server, the browser may serve stale content without validation. `must-revalidate` avoids that. If this directive is present, it means that stale content cannot be served in any case and the data must be re-validated from the server before serving. |
|
||||||
|
|
||||||
```html |
|
||||||
Cache-Control: max-age=3600, public, must-revalidate |
|
||||||
``` |
|
||||||
|
|
||||||
##### proxy-revalidate |
|
||||||
**`proxy-revalidate`** is similar to `must-revalidate` but it specifies the same for shared or proxy caches. In other words `proxy-revalidate` is to `must-revalidate` as `s-maxage` is to `max-age`. But why did they not call it `s-revalidate`?. I have no idea why, if you have any clue please leave a comment below. |
|
||||||
|
|
||||||
##### Mixing Values |
|
||||||
You can combine these directives in different ways to achieve different caching behaviors, however `no-cache/no-store` and `public/private` are mutually exclusive. |
|
||||||
|
|
||||||
If you specify both `no-store` and `no-cache`, `no-store` will be given precedence over `no-cache`. |
|
||||||
|
|
||||||
```html |
|
||||||
; If specified both |
|
||||||
Cache-Control: no-store, no-cache |
|
||||||
|
|
||||||
; Below will be considered |
|
||||||
Cache-Control: no-store |
|
||||||
``` |
|
||||||
|
|
||||||
For `private/public`, for any unauthenticated requests cache is considered `public` and for any authenticated ones cache is considered `private`. |
|
||||||
|
|
||||||
### Validators |
|
||||||
|
|
||||||
Up until now we only discussed how the content is cached and how long the cached content is to be considered fresh but we did not discuss how the client does the validation from the server. Below we discuss the headers used for this purpose. |
|
||||||
|
|
||||||
#### ETag |
|
||||||
|
|
||||||
Etag or "entity tag" was introduced in HTTP/1.1 specs. Etag is just a unique identifier that the server attaches with some resource. This ETag is later on used by the client to make conditional HTTP requests stating `"give me this resource if ETag is not same as the ETag that I have"` and the content is downloaded only if the etags do not match. |
|
||||||
|
|
||||||
Method by which ETag is generated is not specified in the HTTP docs and usually some collision-resistant hash function is used to assign etags to each version of a resource. There could be two types of etags i.e. strong and weak |
|
||||||
|
|
||||||
```html |
|
||||||
ETag: "j82j8232ha7sdh0q2882" - Strong Etag |
|
||||||
ETag: W/"j82j8232ha7sdh0q2882" - Weak Etag (prefixed with `W/`) |
|
||||||
``` |
|
||||||
|
|
||||||
A strong validating ETag means that two resources are **exactly** same and there is no difference between them at all. While a weak ETag means that two resources although not strictly the same but could be considered the same. Weak etags might be useful for dynamic content, for example. |
|
||||||
|
|
||||||
Now you know what etags are but how does the browser make this request? by making a request to server while sending the available Etag in `If-None-Match` header. |
|
||||||
|
|
||||||
Consider the scenario, you opened a web page which loaded a logo image with caching period of 60 seconds and ETag of `abc123xyz`. After about 30 minutes you reload the page, browser will notice that the logo which was fresh for 60 seconds is now stale; it will trigger a request to server, sending the ETag of the stale logo image in `if-none-match` header |
|
||||||
|
|
||||||
```html |
|
||||||
If-None-Match: "abc123xyz" |
|
||||||
``` |
|
||||||
|
|
||||||
Server will then compare this ETag with the ETag of the current version of resource. If both etags are matched, server will send back the response of `304 Not Modified` which will tell the client that the copy that it has is still good and it will be considered fresh for another 60 seconds. If both the etags do not match i.e. the logo has likely changed and client will be sent the new logo which it will use to replace the stale logo that it has. |
|
||||||
|
|
||||||
#### Last-Modified |
|
||||||
|
|
||||||
Server might include the `Last-Modified` header indicating the date and time at which some content was last modified on. |
|
||||||
|
|
||||||
```html |
|
||||||
Last-Modified: Wed, 15 Mar 2017 12:30:26 GMT |
|
||||||
``` |
|
||||||
|
|
||||||
When the content gets stale, client will make a conditional request including the last modified date that it has inside the header called `If-Modified-Since` to server to get the updated `Last-Modified` date; if it matches the date that the client has, `Last-Modified` date for the content is updated to be considered fresh for another `n` seconds. If the received `Last-Modified` date does not match the one that the client has, content is reloaded from the server and replaced with the content that client has. |
|
||||||
|
|
||||||
```html |
|
||||||
If-Modified-Since: Wed, 15 Mar 2017 12:30:26 GMT |
|
||||||
``` |
|
||||||
|
|
||||||
You might be questioning now, what if the cached content has both the `Last-Modified` and `ETag` assigned to it? Well, in that case both are to be used i.e. there will not be any re-downloading of the resource if and only if `ETag` matches the newly retrieved one and so does the `Last-Modified` date. If either the `ETag` does not match or the `Last-Modified` is greater than the one from the server, content has to be downloaded again. |
|
||||||
|
|
||||||
### Where do I start? |
|
||||||
|
|
||||||
Now that we have got *everything* covered, let us put everything in perspective and see how you can use this information. |
|
||||||
|
|
||||||
#### Utilizing Server |
|
||||||
|
|
||||||
Before we get into the possible caching strategies , let me add the fact that most of the servers including Apache and Nginx allow you to implement your caching policy through the server so that you don't have to juggle with headers in your code. |
|
||||||
|
|
||||||
**For example**, if you are using Apache and you have your static content placed at `/static`, you can put below `.htaccess` file in the directory to make all the content in it be cached for an year using below |
|
||||||
|
|
||||||
```html |
|
||||||
# Cache everything for an year |
|
||||||
Header set Cache-Control "max-age=31536000, public" |
|
||||||
``` |
|
||||||
|
|
||||||
You can further use `filesMatch` directive to add conditionals and use different caching strategy for different kinds of files e.g. |
|
||||||
|
|
||||||
```html |
|
||||||
# Cache any images for one year |
|
||||||
<filesMatch ".(png|jpg|jpeg|gif)$"> |
|
||||||
Header set Cache-Control "max-age=31536000, public" |
|
||||||
</filesMatch> |
|
||||||
|
|
||||||
# Cache any CSS and JS files for a month |
|
||||||
<filesMatch ".(css|js)$"> |
|
||||||
Header set Cache-Control "max-age=2628000, public" |
|
||||||
</filesMatch> |
|
||||||
``` |
|
||||||
|
|
||||||
Or if you don't want to use the `.htaccess` file you can modify Apache's configuration file `http.conf`. Same goes for Nginx, you can add the caching information in the location or server block. |
|
||||||
|
|
||||||
#### Caching Recommendations |
|
||||||
|
|
||||||
There is no golden rule or set standards about how your caching policy should look like, each of the application is different and you have to look and find what suits your application the best. However, just to give you a rough idea |
|
||||||
|
|
||||||
- You can have aggressive caching (e.g. cache for an year) on any static content and use fingerprinted filenames (e.g. `style.ju2i90.css`) so that the cache is automatically rejected whenever the files are updated. |
|
||||||
Also it should be noted that you should not cross the upper limit of one year as it [might not be honored](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) |
|
||||||
- Look and decide do you even need caching for any dynamic content, if yes how long it should be. For example, in case of some RSS feed of a blog there could be the caching of a few hours but there couldn't be any caching for inventory items in an ERP. |
|
||||||
- Always add the validators (preferably ETags) in your response. |
|
||||||
- Pay attention while choosing the visibility (private or public) of the cached content. Make sure that you do not accidentally cache any user-specific or sensitive content in any public proxies. When in doubt, do not use cache at all. |
|
||||||
- Separate the content that changes often from the content that doesn't change that often (e.g. in javascript bundles) so that when it is updated it doesn't need to make the whole cached content stale. |
|
||||||
- Test and monitor the caching headers being served by your site. You can use the browser console or `curl -I http://some-url.com` for that purpose. |
|
||||||
|
|
||||||
And that about wraps it up. Stay tuned for more! |
|
@ -1,195 +0,0 @@ |
|||||||
HTTP is the protocol that every web developer should know as it powers the whole web and knowing it is definitely going to help you develop better applications. In this guide, I am going to be discussing what HTTP is, how it came to be, where it is today and how did we get here. |
|
||||||
|
|
||||||
### What is HTTP? |
|
||||||
|
|
||||||
First things first, what is HTTP? HTTP is the `TCP/IP` based application layer communication protocol which standardizes how the client and server communicate with each other. It defines how the content is requested and transmitted across the internet. By application layer protocol, I mean it's just an abstraction layer that standardizes how the hosts (clients and servers) communicate and itself it depends upon `TCP/IP` to get request and response between the client and server. By default TCP port `80` is used but other ports can be used as well. HTTPS, however, uses port `443`. |
|
||||||
|
|
||||||
### HTTP/0.9 – The One Liner (1991) |
|
||||||
|
|
||||||
The first documented version of HTTP was [`HTTP/0.9`](https://www.w3.org/Protocols/HTTP/AsImplemented.html) which was put forward in 1991. It was the simplest protocol ever; having a single method called `GET`. If a client had to access some webpage on the server, it would have made the simple request like below |
|
||||||
|
|
||||||
```html |
|
||||||
GET /index.html |
|
||||||
``` |
|
||||||
And the response from server would have looked as follows |
|
||||||
|
|
||||||
```html |
|
||||||
(response body) |
|
||||||
(connection closed) |
|
||||||
``` |
|
||||||
|
|
||||||
That is, the server would get the request, reply with the HTML in response and as soon as the content has been transferred, the connection will be closed. There were |
|
||||||
|
|
||||||
- No headers |
|
||||||
- `GET` was the only allowed method |
|
||||||
- Response had to be HTML |
|
||||||
|
|
||||||
As you can see, the protocol really had nothing more than being a stepping stone for what was to come. |
|
||||||
|
|
||||||
### HTTP/1.0 - 1996 |
|
||||||
|
|
||||||
In 1996, the next version of HTTP i.e. `HTTP/1.0` evolved that vastly improved over the original version. |
|
||||||
|
|
||||||
Unlike `HTTP/0.9` which was only designed for HTML response, `HTTP/1.0` could now deal with other response formats i.e. images, video files, plain text or any other content type as well. It added more methods (i.e. `POST` and `HEAD`), request/response formats got changed, HTTP headers got added to both the request and responses, status codes were added to identify the response, character set support was introduced, multi-part types, authorization, caching, content encoding and more was included. |
|
||||||
|
|
||||||
Here is how a sample `HTTP/1.0` request and response might have looked like: |
|
||||||
|
|
||||||
```html |
|
||||||
GET / HTTP/1.0 |
|
||||||
Host: kamranahmed.info |
|
||||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) |
|
||||||
Accept: */* |
|
||||||
``` |
|
||||||
|
|
||||||
As you can see, alongside the request, client has also sent its personal information, required response type etc. While in `HTTP/0.9` client could never send such information because there were no headers. |
|
||||||
|
|
||||||
Example response to the request above may have looked like below |
|
||||||
|
|
||||||
```html |
|
||||||
HTTP/1.0 200 OK |
|
||||||
Content-Type: text/plain |
|
||||||
Content-Length: 137582 |
|
||||||
Expires: Thu, 05 Dec 1997 16:00:00 GMT |
|
||||||
Last-Modified: Wed, 5 August 1996 15:55:28 GMT |
|
||||||
Server: Apache 0.84 |
|
||||||
|
|
||||||
(response body) |
|
||||||
(connection closed) |
|
||||||
``` |
|
||||||
|
|
||||||
In the very beginning of the response there is `HTTP/1.0` (HTTP followed by the version number), then there is the status code `200` followed by the reason phrase (or description of the status code, if you will). |
|
||||||
|
|
||||||
In this newer version, request and response headers were still kept as `ASCII` encoded, but the response body could have been of any type i.e. image, video, HTML, plain text or any other content type. So, now that server could send any content type to the client; not so long after the introduction, the term "Hyper Text" in `HTTP` became misnomer. `HMTP` or Hypermedia transfer protocol might have made more sense but, I guess, we are stuck with the name for life. |
|
||||||
|
|
||||||
One of the major drawbacks of `HTTP/1.0` were you couldn't have multiple requests per connection. That is, whenever a client will need something from the server, it will have to open a new TCP connection and after that single request has been fulfilled, connection will be closed. And for any next requirement, it will have to be on a new connection. Why is it bad? Well, let's assume that you visit a webpage having `10` images, `5` stylesheets and `5` javascript files, totalling to `20` items that needs to fetched when request to that webpage is made. Since the server closes the connection as soon as the request has been fulfilled, there will be a series of `20` separate connections where each of the items will be served one by one on their separate connections. This large number of connections results in a serious performance hit as requiring a new `TCP` connection imposes a significant performance penalty because of three-way handshake followed by slow-start. |
|
||||||
|
|
||||||
#### Three-way Handshake |
|
||||||
|
|
||||||
Three-way handshake in its simplest form is that all the `TCP` connections begin with a three-way handshake in which the client and the server share a series of packets before starting to share the application data. |
|
||||||
|
|
||||||
- `SYN` - Client picks up a random number, let's say `x`, and sends it to the server. |
|
||||||
- `SYN ACK` - Server acknowledges the request by sending an `ACK` packet back to the client which is made up of a random number, let's say `y` picked up by server and the number `x+1` where `x` is the number that was sent by the client |
|
||||||
- `ACK` - Client increments the number `y` received from the server and sends an `ACK` packet back with the number `y+1` |
|
||||||
|
|
||||||
Once the three-way handshake is completed, the data sharing between the client and server may begin. It should be noted that the client may start sending the application data as soon as it dispatches the last `ACK` packet but the server will still have to wait for the `ACK` packet to be received in order to fulfill the request. |
|
||||||
|
|
||||||
![](http://i.imgur.com/uERG2G2.png) |
|
||||||
|
|
||||||
> Please note that there is a minor issue with the image, the last `ACK` packet sent by the client to end the handshake contains only `y+1` i.e. it should have been `ACK:y+1` instead of `ACK: x+1, y+1` |
|
||||||
|
|
||||||
However, some implementations of `HTTP/1.0` tried to overcome this issue by introducing a new header called `Connection: keep-alive` which was meant to tell the server "Hey server, do not close this connection, I need it again". But still, it wasn't that widely supported and the problem still persisted. |
|
||||||
|
|
||||||
Apart from being connectionless, `HTTP` also is a stateless protocol i.e. server doesn't maintain the information about the client and so each of the requests has to have the information necessary for the server to fulfill the request on its own without any association with any old requests. And so this adds fuel to the fire i.e. apart from the large number of connections that the client has to open, it also has to send some redundant data on the wire causing increased bandwidth usage. |
|
||||||
|
|
||||||
### HTTP/1.1 - 1999 |
|
||||||
|
|
||||||
After merely 3 years of `HTTP/1.0`, the next version i.e. `HTTP/1.1` was released in 1999; which made alot of improvements over its predecessor. The major improvements over `HTTP/1.0` included |
|
||||||
|
|
||||||
- **New HTTP methods** were added, which introduced `PUT`, `PATCH`, `OPTIONS`, `DELETE` |
|
||||||
|
|
||||||
- **Hostname Identification** In `HTTP/1.0` `Host` header wasn't required but `HTTP/1.1` made it required. |
|
||||||
|
|
||||||
- **Persistent Connections** As discussed above, in `HTTP/1.0` there was only one request per connection and the connection was closed as soon as the request was fulfilled which resulted in acute performance hit and latency problems. `HTTP/1.1` introduced the persistent connections i.e. **connections weren't closed by default** and were kept open which allowed multiple sequential requests. To close the connections, the header `Connection: close` had to be available on the request. Clients usually send this header in the last request to safely close the connection. |
|
||||||
|
|
||||||
- **Pipelining** It also introduced the support for pipelining, where the client could send multiple requests to the server without waiting for the response from server on the same connection and server had to send the response in the same sequence in which requests were received. But how does the client know that this is the point where first response download completes and the content for next response starts, you may ask! Well, to solve this, there must be `Content-Length` header present which clients can use to identify where the response ends and it can start waiting for the next response. |
|
||||||
|
|
||||||
> It should be noted that in order to benefit from persistent connections or pipelining, `Content-Length` header must be available on the response, because this would let the client know when the transmission completes and it can send the next request (in normal sequential way of sending requests) or start waiting for the next response (when pipelining is enabled). |
|
||||||
|
|
||||||
> But there was still an issue with this approach. And that is, what if the data is dynamic and server cannot find the content length before hand? Well in that case, you really can't benefit from persistent connections, could you?! In order to solve this `HTTP/1.1` introduced chunked encoding. In such cases server may omit content-Length in favor of chunked encoding (more to it in a moment). However, if none of them are available, then the connection must be closed at the end of request. |
|
||||||
|
|
||||||
- **Chunked Transfers** In case of dynamic content, when the server cannot really find out the `Content-Length` when the transmission starts, it may start sending the content in pieces (chunk by chunk) and add the `Content-Length` for each chunk when it is sent. And when all of the chunks are sent i.e. whole transmission has completed, it sends an empty chunk i.e. the one with `Content-Length` set to zero in order to identify the client that transmission has completed. In order to notify the client about the chunked transfer, server includes the header `Transfer-Encoding: chunked` |
|
||||||
|
|
||||||
- Unlike `HTTP/1.0` which had Basic authentication only, `HTTP/1.1` included digest and proxy authentication |
|
||||||
- Caching |
|
||||||
- Byte Ranges |
|
||||||
- Character sets |
|
||||||
- Language negotiation |
|
||||||
- Client cookies |
|
||||||
- Enhanced compression support |
|
||||||
- New status codes |
|
||||||
- ..and more |
|
||||||
|
|
||||||
I am not going to dwell about all the `HTTP/1.1` features in this post as it is a topic in itself and you can already find a lot about it. The one such document that I would recommend you to read is [Key differences between `HTTP/1.0` and HTTP/1.1](http://www.ra.ethz.ch/cdstore/www8/data/2136/pdf/pd1.pdf) and here is the link to [original RFC](https://tools.ietf.org/html/rfc2616) for the overachievers. |
|
||||||
|
|
||||||
`HTTP/1.1` was introduced in 1999 and it had been a standard for many years. Although, it improved alot over its predecessor; with the web changing everyday, it started to show its age. Loading a web page these days is more resource-intensive than it ever was. A simple webpage these days has to open more than 30 connections. Well `HTTP/1.1` has persistent connections, then why so many connections? you say! The reason is, in `HTTP/1.1` it can only have one outstanding connection at any moment of time. `HTTP/1.1` tried to fix this by introducing pipelining but it didn't completely address the issue because of the **head-of-line blocking** where a slow or heavy request may block the requests behind and once a request gets stuck in a pipeline, it will have to wait for the next requests to be fulfilled. To overcome these shortcomings of `HTTP/1.1`, the developers started implementing the workarounds, for example use of spritesheets, encoded images in CSS, single humongous CSS/Javascript files, [domain sharding](https://www.maxcdn.com/one/visual-glossary/domain-sharding-2/) etc. |
|
||||||
|
|
||||||
### SPDY - 2009 |
|
||||||
|
|
||||||
Google went ahead and started experimenting with alternative protocols to make the web faster and improving web security while reducing the latency of web pages. In 2009, they announced `SPDY`. |
|
||||||
|
|
||||||
> `SPDY` is a trademark of Google and isn't an acronym. |
|
||||||
|
|
||||||
It was seen that if we keep increasing the bandwidth, the network performance increases in the beginning but a point comes when there is not much of a performance gain. But if you do the same with latency i.e. if we keep dropping the latency, there is a constant performance gain. This was the core idea for performance gain behind `SPDY`, decrease the latency to increase the network performance. |
|
||||||
|
|
||||||
> For those who don't know the difference, latency is the delay i.e. how long it takes for data to travel between the source and destination (measured in milliseconds) and bandwidth is the amount of data transferred per second (bits per second). |
|
||||||
|
|
||||||
The features of `SPDY` included, multiplexing, compression, prioritization, security etc. I am not going to get into the details of SPDY, as you will get the idea when we get into the nitty gritty of `HTTP/2` in the next section as I said `HTTP/2` is mostly inspired from SPDY. |
|
||||||
|
|
||||||
`SPDY` didn't really try to replace HTTP; it was a translation layer over HTTP which existed at the application layer and modified the request before sending it over to the wire. It started to become a defacto standards and majority of browsers started implementing it. |
|
||||||
|
|
||||||
In 2015, at Google, they didn't want to have two competing standards and so they decided to merge it into HTTP while giving birth to `HTTP/2` and deprecating SPDY. |
|
||||||
|
|
||||||
### HTTP/2 - 2015 |
|
||||||
|
|
||||||
By now, you must be convinced that why we needed another revision of the HTTP protocol. `HTTP/2` was designed for low latency transport of content. The key features or differences from the old version of `HTTP/1.1` include |
|
||||||
|
|
||||||
- Binary instead of Textual |
|
||||||
- Multiplexing - Multiple asynchronous HTTP requests over a single connection |
|
||||||
- Header compression using HPACK |
|
||||||
- Server Push - Multiple responses for single request |
|
||||||
- Request Prioritization |
|
||||||
- Security |
|
||||||
|
|
||||||
![](http://i.imgur.com/S85j8gg.png) |
|
||||||
|
|
||||||
|
|
||||||
#### 1. Binary Protocol |
|
||||||
|
|
||||||
`HTTP/2` tends to address the issue of increased latency that existed in HTTP/1.x by making it a binary protocol. Being a binary protocol, it easier to parse but unlike `HTTP/1.x` it is no longer readable by the human eye. The major building blocks of `HTTP/2` are Frames and Streams |
|
||||||
|
|
||||||
##### Frames and Streams |
|
||||||
|
|
||||||
HTTP messages are now composed of one or more frames. There is a `HEADERS` frame for the meta data and `DATA` frame for the payload and there exist several other types of frames (`HEADERS`, `DATA`, `RST_STREAM`, `SETTINGS`, `PRIORITY` etc) that you can check through [the `HTTP/2` specs](https://http2.github.io/http2-spec/#FrameTypes). |
|
||||||
|
|
||||||
Every `HTTP/2` request and response is given a unique stream ID and it is divided into frames. Frames are nothing but binary pieces of data. A collection of frames is called a Stream. Each frame has a stream id that identifies the stream to which it belongs and each frame has a common header. Also, apart from stream ID being unique, it is worth mentioning that, any request initiated by client uses odd numbers and the response from server has even numbers stream IDs. |
|
||||||
|
|
||||||
Apart from the `HEADERS` and `DATA`, another frame type that I think worth mentioning here is `RST_STREAM` which is a special frame type that is used to abort some stream i.e. client may send this frame to let the server know that I don't need this stream anymore. In `HTTP/1.1` the only way to make the server stop sending the response to client was closing the connection which resulted in increased latency because a new connection had to be opened for any consecutive requests. While in HTTP/2, client can use `RST_STREAM` and stop receiving a specific stream while the connection will still be open and the other streams will still be in play. |
|
||||||
|
|
||||||
|
|
||||||
#### 2. Multiplexing |
|
||||||
|
|
||||||
Since `HTTP/2` is now a binary protocol and as I said above that it uses frames and streams for requests and responses, once a TCP connection is opened, all the streams are sent asynchronously through the same connection without opening any additional connections. And in turn, the server responds in the same asynchronous way i.e. the response has no order and the client uses the assigned stream id to identify the stream to which a specific packet belongs. This also solves the **head-of-line blocking** issue that existed in HTTP/1.x i.e. the client will not have to wait for the request that is taking time and other requests will still be getting processed. |
|
||||||
|
|
||||||
|
|
||||||
#### 3. HPACK Header Compression |
|
||||||
|
|
||||||
It was part of a separate RFC which was specifically aimed at optimizing the sent headers. The essence of it is that when we are constantly accessing the server from a same client there is alot of redundant data that we are sending in the headers over and over, and sometimes there might be cookies increasing the headers size which results in bandwidth usage and increased latency. To overcome this, `HTTP/2` introduced header compression. |
|
||||||
|
|
||||||
![](http://i.imgur.com/3IPWXvR.png) |
|
||||||
|
|
||||||
Unlike request and response, headers are not compressed in `gzip` or `compress` etc formats but there is a different mechanism in place for header compression which is literal values are encoded using Huffman code and a headers table is maintained by the client and server and both the client and server omit any repetitive headers (e.g. user agent etc) in the subsequent requests and reference them using the headers table maintained by both. |
|
||||||
|
|
||||||
While we are talking headers, let me add here that the headers are still the same as in HTTP/1.1, except for the addition of some pseudo headers i.e. `:method`, `:scheme`, `:host` and `:path` |
|
||||||
|
|
||||||
|
|
||||||
#### 4. Server Push |
|
||||||
|
|
||||||
Server push is another tremendous feature of `HTTP/2` where the server, knowing that the client is going to ask for a certain resource, can push it to the client without even client asking for it. For example, let's say a browser loads a web page, it parses the whole page to find out the remote content that it has to load from the server and then sends consequent requests to the server to get that content. |
|
||||||
|
|
||||||
Server push allows the server to decrease the round trips by pushing the data that it knows that client is going to demand. How it is done is, server sends a special frame called `PUSH_PROMISE` notifying the client that, "Hey, I am about to send this resource to you! Do not ask me for it." The `PUSH_PROMISE` frame is associated with the stream that caused the push to happen and it contains the promised stream ID i.e. the stream on which the server will send the resource to be pushed. |
|
||||||
|
|
||||||
#### 5. Request Prioritization |
|
||||||
|
|
||||||
A client can assign a priority to a stream by including the prioritization information in the `HEADERS` frame by which a stream is opened. At any other time, client can send a `PRIORITY` frame to change the priority of a stream. |
|
||||||
|
|
||||||
Without any priority information, server processes the requests asynchronously i.e. without any order. If there is priority assigned to a stream, then based on this prioritization information, server decides how much of the resources need to be given to process which request. |
|
||||||
|
|
||||||
#### 6. Security |
|
||||||
|
|
||||||
There was extensive discussion on whether security (through `TLS`) should be made mandatory for `HTTP/2` or not. In the end, it was decided not to make it mandatory. However, most vendors stated that they will only support `HTTP/2` when it is used over `TLS`. So, although `HTTP/2` doesn't require encryption by specs but it has kind of become mandatory by default anyway. With that out of the way, `HTTP/2` when implemented over `TLS` does impose some requirements i.e. `TLS` version `1.2` or higher must be used, there must be a certain level of minimum key sizes, ephemeral keys are required etc. |
|
||||||
|
|
||||||
`HTTP/2` is here and it has already [surpassed SPDY in adaption](http://caniuse.com/#search=http2) which is gradually increasing. `HTTP/2` has alot to offer in terms of performance gain and it is about time we should start using it. |
|
||||||
|
|
||||||
For anyone interested in further details here is the [link to specs](https://http2.github.io/http2-spec) and a link [demonstrating the performance benefits of `HTTP/2`](http://www.http2demo.io/). |
|
||||||
|
|
||||||
And that about wraps it up. Until next time! stay tuned. |
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/jwt-authentication.png)](/guides/jwt-authentication.png) |
|
||||||
|
|
@ -1,72 +0,0 @@ |
|||||||
I have been working on redoing the [roadmaps](https://roadmap.sh) – splitting the skillset based on the seniority levels to make them easier to follow and not scare the new developers away. Since the roadmaps are going to be just about the technical knowledge, I thought it would be a good idea to reiterate and have an article on what I think of different seniority roles. |
|
||||||
|
|
||||||
I have seen many organizations decide the seniority of developers by giving more significance to the years of experience than they should. I have seen developers labeled "Junior" doing the work of Senior Developers and I have seen "Lead" developers who weren't even qualified to be called "Senior". The seniority of a developer cannot just be decided by their age, years of experience or technical knowledge that they have got. There are other factors in play here -- their perception of work, how they interact with their peers and how they approach problems. We discuss these three key factors in detail for each of the seniority levels below. |
|
||||||
|
|
||||||
### Different Seniority Titles |
|
||||||
Different organizations might have different seniority titles but they mainly fall into three categories: |
|
||||||
|
|
||||||
* Junior Developer |
|
||||||
* Mid Level Developer |
|
||||||
* Senior Developer |
|
||||||
|
|
||||||
### Junior Developer |
|
||||||
Junior developers are normally fresh graduates and it's either they don't have or they have minimal industry experience. Not only they have weak coding skills but there are also a few other things that give Junior developers away: |
|
||||||
|
|
||||||
* Their main mantra is "making it work" without giving much attention to how the solution is achieved. To them, a working software and good software are equivalent. |
|
||||||
* They usually require very specific and structured directions to achieve something. They suffer from tunnel vision, need supervision and continuous guidance to be effective team members. |
|
||||||
* Most of the Junior developers just try to live up to the role and, when stuck, they might leave work for a senior developer instead of at least trying to take a stab at something. |
|
||||||
* They don't know about the business side of the company and don't realize how management/sales/marketing/etc think and they don't realize how much rework, wasted effort, and end-user aggravation could be saved by getting to know the business domain. |
|
||||||
* Over-engineering is a major problem, often leading to fragility and bugs. |
|
||||||
* When given a problem, they often try to fix just the current problem a.k.a. fixing the symptoms instead of fixing the root problem. |
|
||||||
* You might notice the "[Somebody Else's Problem](https://en.wikipedia.org/wiki/Somebody_else%27s_problem)" behavior from them. |
|
||||||
* They don't know what or how much they don't know, thanks to the [Dunning–Kruger effect](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect). |
|
||||||
* They don't take initiatives and they might be afraid to work on an unfamiliar codebase. |
|
||||||
* They don't participate in team discussions. |
|
||||||
|
|
||||||
Being a Junior developer in the team is not necessarily a bad thing; since you are just starting out, you are not expected to be a know-it-all person. However, it is your responsibility to learn, gain experience, not get stuck with the "Junior" title and improve yourself. Here are a few tips for Junior developers to help move up the ladder of seniority: |
|
||||||
|
|
||||||
* All sorts of problems can be solved if you work on them long enough. Do not give up if Stack Overflow or an issue on GitHub doesn't have an answer. Saying "I am stuck, but I have tried X, Y, and Z. Do you have any pointers?" to your lead is much better than saying "This is beyond me." |
|
||||||
* Read a lot of code, not just code in the projects that you are working on, but reference/framework source code, open-source. Ask your fellow developers, perhaps on Reddit too, about the good open-source examples for the language/tools of your choice. |
|
||||||
* Do personal side-projects, share them with people, contribute to the open-source community. Reach out to people for help. You will be surprised how much support you can get from the community. I still remember my first open-source project on GitHub from around 6 years ago which was a small PHP script (a library) that fetched details for a given address from Google's Geocoding API. The codebase was super messy, it did not have any tests, did not have any linters or sniffers, and it did not have any CI because I didn't know about any of this at that time. I am not sure how but one kind soul somehow found the project, forked it, refactored it, "modernized" it, added linting, code sniffing, added CI and opened the pull request. This one pull request taught me so many things that I might have never learned that fast on my own because I was still in college, working for a small service-based company and doing just small websites all on my own without knowing what is right and what is not. This one PR on GitHub was my introduction to open-source and I owe everything to that. |
|
||||||
* Avoid what is known as ["Somebody Else's Problem Field"](https://en.wikipedia.org/wiki/Somebody_else%27s_problem) behavior. |
|
||||||
* When given a problem to solve, try to identify the root cause and fix that instead of fixing the symptoms. And remember, not being able to reproduce means not solved. It is solved when you understand why it occurred and why it no longer does. |
|
||||||
* Have respect for the code that was written before you. Be generous when passing judgment on the architecture or the design decisions made in the codebase. Understand that code is often ugly and weird for a reason other than incompetence. Learning to live with and thrive with legacy code is a great skill. Never assume anybody is stupid. Instead, figure out how these intelligent, well-intentioned and experienced people have come to a decision that is stupid now. Approach inheriting legacy code with an "opportunity mindset" rather than a complaining one. |
|
||||||
* It's okay to not know things. You don't need to be ashamed of not knowing things already. There are no stupid questions, ask however many questions that would allow you to work effectively. |
|
||||||
* Don't let yourself be limited by the job title that you have. Keep working on your self-improvement. |
|
||||||
* Do your homework. Predict what’s coming down the pipe. Be involved in the team discussions. Even if you are wrong, you will learn something. |
|
||||||
* Learn about the domain that you are working with. Understand the product end-to-end as an end-user. Do not assume things, ask questions and get things cleared when in doubt. |
|
||||||
* Learn to communicate effectively - soft skills matter. Learn how to write good emails, how to present your work, how to phrase your questions in a thoughtful manner. |
|
||||||
* Sit with the senior developers, watch them work, find a mentor. No one likes a know-it-all. Get hold of your ego and be humble enough to take lessons from experienced people. |
|
||||||
* Don't just blindly follow the advice of "experts", take it with a grain of salt. |
|
||||||
* If you are asked to provide an estimate for some work, do not give an answer unless you have all the details to make a reasonable estimate. If you are forced to do that, pad it 2x or more depending on how much you don't know about what needs to be done for the task to be marked 'done'. |
|
||||||
* Take some time to learn how to use a debugger. Debuggers are quite beneficial when navigating new, undocumented or poorly documented codebase, or to debug weird issues. |
|
||||||
* Avoid saying "it works on my machine" -- yes, I have heard that a lot. |
|
||||||
* Try to turn any feelings of inadequacy or imposter syndrome into energy to push yourself forward and increase your skills and knowledge. |
|
||||||
|
|
||||||
### Mid Level Developers |
|
||||||
The next level after the Junior developers is Mid Level developers. They are technically stronger than the Junior developers and can work with minimal supervision. They still have some issues to address in order to jump to Senior level. |
|
||||||
|
|
||||||
Intermediate developers are more competent than the Junior developer. They start to see the flaws in their old codebase. They gain the knowledge but they get trapped into the next chain i.e. messing things up while trying to do them "the right way" e.g. hasty abstractions, overuse or unnecessary usage of Design Patterns -- they may be able to provide solution faster than the Junior developers but the solution might put you into another rabbit-hole in the long run. Without supervision, they might delay the execution while trying to "do things properly". They don't know when to make tradeoffs and they still don't know when to be dogmatic and when to be pragmatic. They can easily become attached to their solution, become myopic, and be unable to take feedback. |
|
||||||
|
|
||||||
Mid-level developers are quite common. Most of the organizations wrongly label them as "Senior Developers". However, they need further mentoring in order to become Senior Developers. The next section describes the responsibilities of a senior developer and how you can become one. |
|
||||||
|
|
||||||
### Senior Developers |
|
||||||
Senior developers are the next level after the Mid-level developers. They are the people who can get things done on their own without any supervision and without creating any issues down the road. They are more mature, have gained experience by delivering both good and bad software in the past and have learned from it — they know how to be pragmatic. Here is the list of things that are normally expected of a Senior Developer: |
|
||||||
|
|
||||||
* With their past experiences, mistakes made, issues faced by over-designed or under-designed software, they can foresee the problems and persuade the direction of the codebase or the architecture. |
|
||||||
* They don't have a "Shiny-Toy" syndrome. They are pragmatic in the execution. They can make the tradeoffs when required, and they know why. They know where to be dogmatic and where to be pragmatic. |
|
||||||
* They have a good picture of the field, know what the best tool for the job is in most cases (even if they don't know the tool). They have the innate ability to pick up a new tool/language/paradigm/etc in order to solve a problem that requires it. |
|
||||||
* They are aware they're on a team. They view it as a part of their responsibility to mentor others. This can range from pair programming with junior devs to taking un-glorious tasks of writing docs or tests or whatever else needs to be done. |
|
||||||
* They have a deep understanding of the domain - they know about the business side of the company and realize how management/sales/marketing/etc think and benefit from their knowledge of the business domain during the development. |
|
||||||
* They don't make empty complaints, they make judgments based on the empirical evidence and they have suggestions for solutions. |
|
||||||
* They think much more than just code - they know that their job is to provide solutions to the problems and not just to write code. |
|
||||||
* They have the ability to take on large ill-defined problems, define them, break them up, and execute the pieces. A senior developer can take something big and abstract, and run with it. They will come up with a few options, discuss them with the team and implement them. |
|
||||||
* They have respect for the code that was written before them. They are generous when passing judgment on the architecture or the design decisions made in the codebase. They approach inheriting legacy code with an "opportunity mindset" rather than a complaining one. |
|
||||||
* They know how to give feedback without hurting anyone. |
|
||||||
|
|
||||||
### Conclusion |
|
||||||
All teams are made up of a mix of all these seniority roles. Being content with your role is a bad thing and you should always strive to improve yourself for the next step. This article is based on my beliefs and observations in the industry. Lots of companies care more for the years of experience to decide the seniority which is a crappy metric -- you don't gain experience just by spending years. You gain it by continuously solving different sorts of problems, irrespective of the number of years you spend in the industry. I have seen fresh graduates having no industry experience get up to speed quickly and producing work of a Senior Engineer and I have seen Senior developers labeled "senior" merely because of their age and "years of experience". |
|
||||||
|
|
||||||
The most important traits that you need to have in order to step up in your career are: not settling with mediocrity, having an open mindset, being humble, learning from your mistakes, working on the challenging problems and having an opportunity mindset rather than a complaining one. |
|
||||||
|
|
||||||
With that said, this post comes to an end. What are your thoughts on the levels of seniority of developers? Feel free to send improvements to this guide. Until next time, stay tuned! |
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/oauth.png)](/guides/oauth.png) |
|
||||||
|
|
@ -1,5 +0,0 @@ |
|||||||
One of my favorite pastimes is going through the history of my favorite projects to learn how they grew over time or how certain features were implemented. |
|
||||||
|
|
||||||
The image below describes how I do that in WebStorm. |
|
||||||
|
|
||||||
[![](/guides/project-history.png)](/guides/project-history.png) |
|
@ -1,47 +0,0 @@ |
|||||||
The Internet has connected people across the world using social media and audio/video calling features along with providing an overabundance of knowledge and tools. All this comes with an inherent danger of security and privacy breaches. In this guide, we will talk about **proxies** that play a vital role in mitigating these risks. We will cover the following topics in this guide: |
|
||||||
|
|
||||||
- [Proxy Server](#proxy-server) |
|
||||||
- [Forward Proxy Server](#forward-proxy-server) |
|
||||||
- [Reverse Proxy Server](#reverse-proxy-server) |
|
||||||
- [Summary](#summary) |
|
||||||
|
|
||||||
## Proxy Server |
|
||||||
|
|
||||||
***Every web request which is sent from the client to a web server goes through some type of proxy server.*** A proxy server acts as a gateway between client *(you)* and the internet and separates end-users from the websites you browse. It replaces the source IP address of the web request with the proxy server's IP address and then forwards it to the web server. The web server is unaware of the client, it only sees the proxy server. |
|
||||||
|
|
||||||
![Proxy Server Description](/guides/proxy/proxy-example.png) |
|
||||||
> NOTE: This is not an accurate description but rather just an illustration. |
|
||||||
|
|
||||||
Proxy servers serve as a single point of control making it easier to enforce security policies. It also provides caching mechanism which stores the requested web pages on the proxy server to improve performance. If the requested web-page is available in cache memory then instead of forwarding the request to the web-server it will send the cached webpage back to the client. This **saves big companies thousands of dollars** by reducing the load on their servers as their website is visited by millions of users every day. |
|
||||||
|
|
||||||
## Forward Proxy Server |
|
||||||
|
|
||||||
A forward proxy is generally implemented on the client side and **sits in front of multiple clients** or client sources. Forward proxy servers are mainly used by companies to **manage the internet usage** of their employees and **restrict content**. It is also used as a **firewall** to secure the company's network by blocking any request which would pose threat to the company's network. Proxy servers are also used to **bypass geo-restriction** and browse content that might be blocked in the user's country. It enables users to **browse anonymously**, as the proxy server masks their details from the website's servers. |
|
||||||
|
|
||||||
![Forward Proxy Description](/guides/proxy/forward-proxy.png) |
|
||||||
> NOTE: This is not an accurate description but rather just an illustration |
|
||||||
|
|
||||||
## Reverse Proxy Server |
|
||||||
|
|
||||||
Reverse proxy servers are implemented on the **server side** instead of the client side. It **sits in front of multiple webservers** and manages incoming requests by forwarding them to the web servers. It provides anonymity for the **back-end web servers and not the client**. Reverse proxy servers are generally used to perform tasks such as **authentication, content caching, and encryption/decryption** on behalf of the web server. These tasks would **hog CPU cycles** on the web server and degrade the performance of the website by introducing a high amount of delay in loading the webpage. Reverse proxies are also used as **load balancers** to distribute the incoming traffic efficiently among the web servers but it is **not optimized** for this task. In essence, a reverse proxy server is a gateway to a web-server or group of web-servers. |
|
||||||
|
|
||||||
![Reverse Proxy Description](/guides/proxy/reverse-proxy.png) |
|
||||||
> NOTE: This is not an accurate description but rather just an illustration. Red lines represent the server's response and black lines represent the initial request from client(s). |
|
||||||
|
|
||||||
## Summary |
|
||||||
|
|
||||||
A proxy server acts as a gateway between client *(you)* and the internet and separates end-users from the websites you browse. ***The position of the proxy server on the network determines whether it is a forward or a reverse proxy server***. A Forward proxy is implemented on the client side and **sits in front of multiple clients** or client sources and forwards requests to the web server. Reverse proxy servers are implemented on the **server side** it **sits in front of multiple webservers** and manage the incoming requests by forwarding them to the web servers. |
|
||||||
|
|
||||||
If all this was too much to take in, I have a simple analogy for you. |
|
||||||
|
|
||||||
At a restaurant, the waiter/waitress takes your order and gives it to the kitchen head chef. The head chef then calls out the order and assigns tasks to everyone in the kitchen. |
|
||||||
|
|
||||||
In this analogy: |
|
||||||
|
|
||||||
* You are the client |
|
||||||
* Your order is the web request |
|
||||||
* Waiter/Waitress is your forward proxy server |
|
||||||
* Kitchen head chef is the reverse proxy server |
|
||||||
* Other chefs working in the kitchen are the web servers |
|
||||||
|
|
||||||
With that said our guide comes to an end. Thank you for reading and feel free to submit any updates to the guide using the links below. |
|
@ -1,4 +0,0 @@ |
|||||||
Random numbers are everywhere from computer games to lottery systems, graphics software, statistical sampling, computer simulation and cryptography. Graphic below is a quick explanation to how the random numbers are generated and why they may not be truly random. |
|
||||||
|
|
||||||
[![](/guides/random-numbers.png)](/guides/random-numbers.png) |
|
||||||
|
|
@ -1,4 +0,0 @@ |
|||||||
The chart below aims to give you a really basic understanding of how the capability of a DBMS is increased to handle a growing amount of load. |
|
||||||
|
|
||||||
[![](/guides/scaling-databases.svg)](/guides/scaling-databases.svg) |
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/session-authentication.png)](/guides/session-authentication.png) |
|
||||||
|
|
@ -1,199 +0,0 @@ |
|||||||
HTTP is the internet protocol that standardizes how clients and servers interact with each other. When you open a website, among other things, HTTP is the protocol that helps load the website in the browser. |
|
||||||
|
|
||||||
## HTTP is Stateless |
|
||||||
HTTP is a stateless protocol which means that each request made from the client to the server is treated as a standalone request; neither the client nor the server keeps track of the subsequent requests. Sessions allow you to change that; with sessions, the server has a way to associate some information with the client so that when the same client requests the server, it can retrieve that information. |
|
||||||
|
|
||||||
In this guide, we will learn what is Session-Based Authentication and how to implement it in Node.js. We also have a separate [visual guide on Session-Based Authentication](/guides/session-authentication) as well that explains the topic visually. |
|
||||||
|
|
||||||
## What is Session-Based Authentication? |
|
||||||
Session-based authentication is a stateful authentication technique where we use sessions to keep track of the authenticated user. Here is how Session Based Authentication works: |
|
||||||
|
|
||||||
* User submits the login request for authentication. |
|
||||||
* Server validates the credentials. If the credentials are valid, the server initiates a session and stores some information about the client. This information can be stored in memory, file system, or database. The server also generates a unique identifier that it can later use to retrieve this session information from the storage. Server sends this unique session identifier to the client. |
|
||||||
* Client saves the session id in a cookie and this cookie is sent to the server in each request made after the authentication. |
|
||||||
* Server, upon receiving a request, checks if the session id is present in the request and uses this session id to get information about the client. |
|
||||||
|
|
||||||
And that is how session-based authentication works. |
|
||||||
|
|
||||||
## Session-Based Authentication in Node.js |
|
||||||
Now that we know what session-based authentication is, let's see how we can implement session-based authentication in Node.js. |
|
||||||
|
|
||||||
Please note that, for the sake of simplicity, I have intentionally kept the project strictly relevant to the Session Based Authentication and have left out a lot of details that a production-ready application may require. Also, if you don't want to follow along, project [codebase can be found on GitHub](https://github.com/kamranahmedse/node-session-auth-example). |
|
||||||
|
|
||||||
First things first, create an empty directory that will be holding our application. |
|
||||||
|
|
||||||
```shell |
|
||||||
mkdir session-auth-example |
|
||||||
``` |
|
||||||
Now run the following command to setup a sample `package.json` file: |
|
||||||
```shell |
|
||||||
npm init -y |
|
||||||
``` |
|
||||||
Next, we need to install the dependencies: |
|
||||||
```shell |
|
||||||
npm install express express-session |
|
||||||
``` |
|
||||||
`Express` is the application framework, and `express-session` is the package that helps work with sessions easily. |
|
||||||
|
|
||||||
### Setting up the server |
|
||||||
Now create an `index.js` file at the root of the project with the following content: |
|
||||||
|
|
||||||
```javascript |
|
||||||
const express = require('express'); |
|
||||||
const sessions = require('express-session'); |
|
||||||
|
|
||||||
const app = express(); |
|
||||||
|
|
||||||
app.use(sessions({ |
|
||||||
secret: "some secret", |
|
||||||
cookie: { |
|
||||||
maxAge: 1000 * 60 * 60 * 24 // 24 hours |
|
||||||
}, |
|
||||||
resave: true, |
|
||||||
saveUninitialized: false, |
|
||||||
})); |
|
||||||
|
|
||||||
app.use(express.json()); |
|
||||||
app.use(express.urlencoded({extended: true})); |
|
||||||
|
|
||||||
// @todo register routes |
|
||||||
|
|
||||||
app.listen(3000, () => { |
|
||||||
console.log(`Server Running at port 3000`); |
|
||||||
}); |
|
||||||
``` |
|
||||||
The important piece to note here is the `express-session` middleware registration which automatically handles the session initialization, cooking parsing and session data retrieval, and so on. In our example here, we are passing the following configuration options: |
|
||||||
* `secret`: This is used to sign the session ID cookie. Using a secret that cannot be guessed will reduce the ability to hijack a session. |
|
||||||
* `cookie`: Object containing the configuration for session id cookie. |
|
||||||
* `resave`: Forces the session to be saved back to the session store, even if the session data was never modified during the request. |
|
||||||
* `saveUninitialized`: Forces an "uninitialized" session to be saved to the store, i.e., saves a session to the store even if the session was not initiated. |
|
||||||
|
|
||||||
Another important option is `store` which we can configure to change how/where the session data is stored on the server. By default, this data is stored in the memory, i.e., `MemoryStore`. |
|
||||||
|
|
||||||
Look at the [express-session documentation](https://github.com/expressjs/session) to learn more about the available options. |
|
||||||
|
|
||||||
### Creating Handlers |
|
||||||
Create a directory called the `handlers` at the project's root. This is the directory where we will be placing all the route-handling functions. |
|
||||||
|
|
||||||
Now let's create the homepage route, which will show the welcome message and a link to log out for the logged-in users and redirect to the login screen for the logged-out users. Create a file at `handlers/home.js` with the following content. |
|
||||||
|
|
||||||
```javascript |
|
||||||
module.exports = function HomeHandler(req, res) { |
|
||||||
if (!req.session.userid) { |
|
||||||
return res.redirect('/login'); |
|
||||||
} |
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/HTML') |
|
||||||
res.write(` |
|
||||||
<h1>Welcome back ${req.session.userid}</h1> |
|
||||||
<a href="/logout">Logout</a> |
|
||||||
`); |
|
||||||
|
|
||||||
res.end() |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
At the top of this function, you will notice the check `req.session.userid`. `req.session` is automatically populated using the session cookie by the `express-session` middleware that we registered earlier. `req.session.userid` is one of the data fields that we will set to store the `userid` of the logged in user. |
|
||||||
|
|
||||||
Next, we need to register this handler with a route. Open the `index.js` file at the root of the project and register the following route: |
|
||||||
|
|
||||||
```javascript |
|
||||||
const HomeHandler = require('./handlers/home.js'); |
|
||||||
|
|
||||||
app.get('/', HomeHandler); |
|
||||||
``` |
|
||||||
|
|
||||||
Next, we have the login page, redirecting the user to the home screen if the user is logged in or showing the login form. Create a file at `handlers/login.js` with the following content: |
|
||||||
|
|
||||||
```javascript |
|
||||||
module.exports = function LoginHandler(req, res) { |
|
||||||
if (req.session.userid) { |
|
||||||
return res.redirect('/'); |
|
||||||
} |
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/HTML') |
|
||||||
res.write(` |
|
||||||
<h1>Login</h1> |
|
||||||
<form method="post" action="/process-login"> |
|
||||||
<input type="text" name="username" placeholder="Username" /> <br> |
|
||||||
<input type="password" name="password" placeholder="Password" /> <br> |
|
||||||
<button type="submit">Login</button> |
|
||||||
</form> |
|
||||||
`); |
|
||||||
|
|
||||||
res.end(); |
|
||||||
} |
|
||||||
``` |
|
||||||
Again, at the top of the function, we are simply checking if we have `userid` in the session (which means the user is logged in). If the user is logged in, we redirect them to the homepage; if not, we show the login screen. In the login form, we have the method of `post`, and we submit the form to `/process-login`. Please note that, for the sake of simplicity, we have a simple HTML string returned in the response, but in a real-world application, you will probably have a separate view file. |
|
||||||
|
|
||||||
Let's first register this page and then implement `/process-login` endpoint. Open the `index.js` file from the root of the project and register the following route: |
|
||||||
|
|
||||||
```javascript |
|
||||||
const LoginHandler = require('./handlers/login.js'); |
|
||||||
|
|
||||||
app.get('/login', LoginHandler); |
|
||||||
``` |
|
||||||
|
|
||||||
Next, we have to implement the functionality to process the login form submissions. Create a file at `handlers/process-login.js` with the following content: |
|
||||||
|
|
||||||
```javascript |
|
||||||
module.exports = function processLogin(req, res) { |
|
||||||
if (req.body.username !== 'admin' || req.body.password !== 'admin') { |
|
||||||
return res.send('Invalid username or password); |
|
||||||
} |
|
||||||
|
|
||||||
req.session.userid = req.body.username; |
|
||||||
|
|
||||||
res.redirect('/'); |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
As you can see, we are simply checking that the username and password should both be `admin` and `admin` for a user to authenticate successfully. Upon finding valid credentials, we set the `userid` in the session by updating `req.session.userid`. Similarly, you can set any data in the session. For example, if we wanted to store the user role, we would do the following: |
|
||||||
|
|
||||||
```javascript |
|
||||||
req.session.role = 'admin' |
|
||||||
``` |
|
||||||
|
|
||||||
And later access this value out of the session anywhere in the subsequent requests. |
|
||||||
|
|
||||||
Register this route in the `index.js` file at the root of the project: |
|
||||||
|
|
||||||
```javascript |
|
||||||
const ProcessLoginHandler = require('./handlers/process-login.js'); |
|
||||||
|
|
||||||
app.post('/process-login', ProcessLoginHandler); |
|
||||||
``` |
|
||||||
|
|
||||||
Finally, we have the logout functionality. Create a file at `handlers/logout.js` with the following content: |
|
||||||
|
|
||||||
```javascript |
|
||||||
module.exports = function Logout(req, res) { |
|
||||||
req.session.destroy(); |
|
||||||
res.redirect('/'); |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
We reset the session by calling `req.session.destroy()` and then redirecting the user to the homepage. Register the logout handler in the `index.js` file using the following: |
|
||||||
|
|
||||||
```javascript |
|
||||||
const LogoutHandler = require('./handlers/logout.js'); |
|
||||||
|
|
||||||
app.get('/logout', LogoutHandler); |
|
||||||
``` |
|
||||||
|
|
||||||
## Running the Application |
|
||||||
Open the `package.json` file and register the `start` script as follows: |
|
||||||
|
|
||||||
```javascript |
|
||||||
"scripts": { |
|
||||||
"start": "node index.js" |
|
||||||
}, |
|
||||||
``` |
|
||||||
|
|
||||||
Now you can start the application by running the following command: |
|
||||||
|
|
||||||
```shell |
|
||||||
npm run start |
|
||||||
``` |
|
||||||
|
|
||||||
Now, if you open up your browser and visit the project at `http://localhost:3000` you will be able to see the Session-Based Authentication in action. |
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/ssl-tls-https-ssh.png)](/guides/ssl-tls-https-ssh.png) |
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/sso.png)](/guides/sso.png) |
|
||||||
|
|
@ -1,8 +0,0 @@ |
|||||||
# Threads and Concurrency |
|
||||||
|
|
||||||
A thread is an execution context in which the instructions to the CPU can be scheduled and executed independently of the parent process. Concurrency is the concept of multiple threads in a shared memory space being computed simultaneously (or intermittently executed in succession to provide that illusion). Concurrency allows multiple processes to execute at once and can apply to programming languages as well as operating systems. |
|
||||||
|
|
||||||
<ResourceGroupTitle>Free Content</ResourceGroupTitle> |
|
||||||
<BadgeLink badgeText='Watch' href='https://www.youtube.com/watch?v=olYdb0DdGtM'>Threading Tutorial #1 - Concurrency, Threading and Parallelism</BadgeLink> |
|
||||||
<BadgeLink colorScheme='yellow' badgeText='Read' href='https://medium.com/@akhandmishra/operating-system-threads-and-concurrency-aec2036b90f8'>Operating System: Threads and Concurrency</BadgeLink> |
|
||||||
<BadgeLink colorScheme='yellow' badgeText='Read' href='https://web.mit.edu/6.005/www/fa14/classes/17-concurrency/'>Reading 17: Concurrency</BadgeLink> |
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/token-authentication.png)](/guides/token-authentication.png) |
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/unfamiliar-codebase.png)](/guides/unfamiliar-codebase.png) |
|
||||||
|
|
@ -1,11 +0,0 @@ |
|||||||
> **Roadmap is not ready yet**. Please check back later or [subscribe to get notified](/signup). |
|
||||||
|
|
||||||
While we prepare the roadmap, follow this simple advice to learn anything |
|
||||||
|
|
||||||
> Just **pick a project and start working on it**, you will learn all that you need along the way. |
|
||||||
|
|
||||||
**→** [All Roadmaps](/roadmaps) • [Programming guides](/guides) • [Subscribe](/signup) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/web-vitals.png)](/guides/web-vitals.png) |
|
||||||
|
|
@ -1,62 +0,0 @@ |
|||||||
Since the explosive growth of web-based applications, every developer stands to benefit from understanding how the Internet works. Through this article and its accompanying introductory series of short videos about the Internet from [code.org](https://code.org), you will learn the basics of the Internet and how it works. After going through this article, you will be able to answer the following questions: |
|
||||||
|
|
||||||
* What is the Internet? |
|
||||||
* How does the information move on the internet? |
|
||||||
* How do the networks talk to each other and the protocols involved? |
|
||||||
* What's the relationship between packets, routers, and reliability? |
|
||||||
* HTTP and the HTML – How are you viewing this webpage in your browser? |
|
||||||
* How is the information transfer on the internet made secure? |
|
||||||
* What is cybersecurity and what are some common internet crimes? |
|
||||||
|
|
||||||
## What is the Internet? |
|
||||||
|
|
||||||
The Internet is a global network of computers connected to each other which communicate through a standardized set of protocols. |
|
||||||
|
|
||||||
In the video below, Vint Cerf, one of the "fathers of the internet," explains the history of how the Internet works and how no one person or organization is really in charge of it. |
|
||||||
|
|
||||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/Dxcc6ycZ73M" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
|
||||||
|
|
||||||
|
|
||||||
## Wires, Cables, and Wi-Fi |
|
||||||
|
|
||||||
Information on the Internet moves from one computer to another in the form of bits over various mediums, including Ethernet cables, fiber optic cables, and wireless signals (i.e., radio waves). |
|
||||||
|
|
||||||
In the video linked below, you will learn about the different mediums for data transfer on the Internet and the pros and cons for each. |
|
||||||
|
|
||||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/ZhEf7e4kopM" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
|
||||||
|
|
||||||
## IP Addresses and DNS |
|
||||||
|
|
||||||
Now that you know about the physical medium for the data transfer over the internet, it's time to learn about the protocols involved. How does the information traverse from one computer to another in this massive global network of computers? |
|
||||||
|
|
||||||
In the video below, you will get a brief introduction to IP, DNS, and how these protocols make the Internet work. |
|
||||||
|
|
||||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/5o8CwafCxnU" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
|
||||||
|
|
||||||
## Packets, Routing, and Reliability |
|
||||||
|
|
||||||
Information transfer on the Internet from one computer to another does not need to follow a fixed path; in fact, it may change paths during the transfer. This information transfer is done in the form of packets and these packets may follow different routes depending on certain factors. |
|
||||||
|
|
||||||
In this video, you will learn about how the packets of information are routed from one computer to another to reach the destination. |
|
||||||
|
|
||||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/AYdF7b3nMto" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
|
||||||
|
|
||||||
## HTTP and HTML |
|
||||||
|
|
||||||
HTTP is the standard protocol by which webpages are transferred over the Internet. The video below is a brief introduction to HTTP and how web browsers load websites for you. |
|
||||||
|
|
||||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/kBXQZMmiA4s" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
|
||||||
|
|
||||||
## Encryption and Public Keys |
|
||||||
|
|
||||||
Cryptography is what keeps our communication secure on the Internet. In this short video, you will learn the basics of cryptography, SSL/TLS, and how they help make the communication on the Internet secure. |
|
||||||
|
|
||||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/ZghMPWGXexs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
|
||||||
|
|
||||||
## Cybersecurity and Crime |
|
||||||
|
|
||||||
Cybersecurity refers to the protective measures against criminal activity accomplished through using a network, technological devices, and the internet.In this video, you will learn about the basics of cybersecurity and common cybercrimes. |
|
||||||
|
|
||||||
<iframe width="100%" height="400" src="https://www.youtube.com/embed/AuYNXgO_f3Y" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> |
|
||||||
|
|
||||||
And that wraps it up for this article. To learn more about the Internet, [Kamran Ahmed](https://twitter.com/kamranahmedse) has a nice little guide on [DNS: How a website is found on the Internet](/guides/dns-in-one-picture). Also, go through the episodes of [howdns.works](https://howdns.works/) and read this [cartoon intro to DNS over HTTPS](https://hacks.mozilla.org/2018/05/a-cartoon-intro-to-dns-over-https/). |
|
@ -1,2 +0,0 @@ |
|||||||
[![](/guides/sli-slo-sla.jpeg)](/guides/sli-slo-sla.jpeg) |
|
||||||
|
|