feat: redesign roadmap page header and add upcoming projects functionality (#6347)

* Redesign the header

* Responsiveness of the roadmap header

* Fix spacing

* Redesign roadmap header

* Add projects badge

* Update badge

* Add screen for projects

* UI flicker fix

* Add question for system design

* Code formatting
pull/6348/head
Kamran Ahmed 4 months ago committed by GitHub
parent 5a052d0db2
commit 1087e1a935
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 40
      src/components/DownloadRoadmapButton.tsx
  2. 5
      src/components/EditorRoadmap/EditorRoadmap.tsx
  3. 2
      src/components/FeaturedItems/MarkFavorite.tsx
  4. 10
      src/components/FrameRenderer/FrameRenderer.astro
  5. 60
      src/components/Projects/EmptyProjects.tsx
  6. 52
      src/components/ResourceProgressStats.astro
  7. 228
      src/components/RoadmapHeader.astro
  8. 39
      src/components/RoadmapHint.astro
  9. 24
      src/components/RoadmapTitleQuestion.tsx
  10. 2
      src/components/ShareRoadmapButton.tsx
  11. 69
      src/components/TabLink.tsx
  12. 8
      src/data/roadmaps/system-design/system-design.md
  13. 6
      src/lib/resource-progress.ts
  14. 100
      src/pages/[roadmapId]/index.astro
  15. 84
      src/pages/[roadmapId]/projects.astro
  16. 4
      src/styles/global.css

@ -0,0 +1,40 @@
import { Download } from 'lucide-react';
import { isLoggedIn } from '../lib/jwt.ts';
import { useEffect, useState } from 'react';
import { showLoginPopup } from '../lib/popup.ts';
type DownloadRoadmapButtonProps = {
roadmapId: string;
};
export function DownloadRoadmapButton(props: DownloadRoadmapButtonProps) {
const { roadmapId } = props;
const [url, setUrl] = useState<string>('#');
useEffect(() => {
if (isLoggedIn()) {
setUrl(`/pdfs/roadmaps/${roadmapId}.pdf`);
}
}, []);
return (
<a
className="inline-flex items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label="Download Roadmap"
target="_blank"
href={url}
onClick={(e) => {
if (isLoggedIn()) {
return;
}
e.preventDefault();
showLoginPopup();
}}
>
<Download className="h-4 w-4" />
<span className="ml-2 hidden sm:inline">Download</span>
</a>
);
}

@ -68,14 +68,13 @@ export function EditorRoadmap(props: EditorRoadmapProps) {
: undefined : undefined
} }
className={ className={
'flex aspect-[var(--aspect-ratio)] w-full flex-col justify-center' 'mt-5 flex aspect-[var(--aspect-ratio)] w-full flex-col justify-center'
} }
> >
<div className="flex w-full justify-center"> <div className="flex w-full justify-center">
<Spinner <Spinner
innerFill="#2563eb"
outerFill="#E5E7EB"
className="h-6 w-6 animate-spin sm:h-12 sm:w-12" className="h-6 w-6 animate-spin sm:h-12 sm:w-12"
isDualRing={false}
/> />
</div> </div>
</div> </div>

@ -103,7 +103,7 @@ export function MarkFavorite({
className || 'absolute right-1.5 top-1.5 z-30 focus:outline-0' className || 'absolute right-1.5 top-1.5 z-30 focus:outline-0'
}`} }`}
> >
{isLoading ? <Spinner /> : <FavoriteIcon isFavorite={isFavorite} />} {isLoading ? <Spinner isDualRing={false} /> : <FavoriteIcon isFavorite={isFavorite} />}
</button> </button>
); );
} }

@ -1,7 +1,8 @@
--- ---
import Loader from '../Loader.astro'; import Loader from '../Loader.astro';
import './FrameRenderer.css'; import './FrameRenderer.css';
import { ProgressNudge } from "./ProgressNudge"; import { Spinner } from '../ReactIcons/Spinner';
import { ProgressNudge } from './ProgressNudge';
export interface Props { export interface Props {
resourceType: 'roadmap' | 'best-practice'; resourceType: 'roadmap' | 'best-practice';
@ -16,6 +17,7 @@ const { resourceId, resourceType, dimensions = null } = Astro.props;
--- ---
<div <div
class='mt-3.5'
id='resource-svg-wrap' id='resource-svg-wrap'
style={dimensions style={dimensions
? `--aspect-ratio:${dimensions.width}/${dimensions.height}` ? `--aspect-ratio:${dimensions.width}/${dimensions.height}`
@ -28,6 +30,10 @@ const { resourceId, resourceType, dimensions = null } = Astro.props;
</div> </div>
</div> </div>
<ProgressNudge resourceId={resourceId} resourceType={resourceType} client:only="react" /> <ProgressNudge
resourceId={resourceId}
resourceType={resourceType}
client:only='react'
/>
<script src='./renderer.ts'></script> <script src='./renderer.ts'></script>

@ -0,0 +1,60 @@
import { Bell, Check, FolderKanbanIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { cn } from '../../lib/classname.ts';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { isLoggedIn } from '../../lib/jwt.ts';
import { showLoginPopup } from '../../lib/popup.ts';
export function EmptyProjects() {
const [isSubscribed, setIsSubscribed] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsSubscribed(isLoggedIn());
setIsLoading(false);
}, []);
return (
<div className="flex flex-col items-center justify-center">
<FolderKanbanIcon className="h-14 w-14 text-gray-300" strokeWidth={1.5} />
<h2 className="mb-0.5 mt-2 text-center text-base font-medium text-gray-900 sm:text-xl">
<span className="hidden sm:inline">Projects are coming soon!</span>
<span className="inline sm:hidden">Coming soon!</span>
</h2>
<p className="mb-3 text-balance text-center text-sm text-gray-500 sm:text-base">
Sign up to get notified when projects are available.
</p>
<button
onClick={() => {
if (isSubscribed) {
return;
}
showLoginPopup();
}}
className={cn(
'flex items-center rounded-md bg-gray-800 py-1.5 pl-3 pr-4 text-xs text-white opacity-0 transition-opacity duration-500 hover:bg-black sm:text-sm',
{
'cursor-default bg-gray-300 text-black hover:bg-gray-300':
isSubscribed,
'opacity-100': !isLoading,
},
)}
>
{!isSubscribed && (
<>
<Bell className="mr-2 h-4 w-4" />
Signup to get Notified
</>
)}
{isSubscribed && (
<>
<Check className="mr-2 h-4 w-4" />
We will notify you by email
</>
)}
</button>
</div>
);
}

@ -5,20 +5,15 @@ import { ProgressShareButton } from './UserProgress/ProgressShareButton';
export interface Props { export interface Props {
resourceId: string; resourceId: string;
resourceType: ResourceType; resourceType: ResourceType;
hasSecondaryBanner?: boolean;
} }
const { hasSecondaryBanner = false, resourceId, resourceType } = Astro.props; const { resourceId, resourceType } = Astro.props;
--- ---
<div <div
data-progress-nums-container data-progress-nums-container
class:list={[ class:list={[
'hidden sm:flex justify-between px-2 bg-white items-center py-1.5 relative striped-loader bg-white', 'striped-loader relative flex items-center justify-between rounded-md bg-white px-3 py-2.5',
{
'rounded-tl-md rounded-tr-md': hasSecondaryBanner,
'rounded-md': !hasSecondaryBanner,
},
]} ]}
> >
<p <p
@ -26,24 +21,12 @@ const { hasSecondaryBanner = false, resourceId, resourceType } = Astro.props;
data-progress-nums data-progress-nums
> >
<span <span
class='mr-2.5 rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900' class='mr-2.5 hidden rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900 sm:block'
> >
<span data-progress-percentage>0</span>% Done <span data-progress-percentage>0</span>% Done
</span> </span>
<span class='itesm-center hidden md:flex'> <span>
<span><span data-progress-done>0</span> completed</span><span
class='mx-1.5 text-gray-400'>&middot;</span
>
<span><span data-progress-learning>0</span> in progress</span><span
class='mx-1.5 text-gray-400'>&middot;</span
>
<span><span data-progress-skipped>0</span> skipped</span><span
class='mx-1.5 text-gray-400'>&middot;</span
>
<span><span data-progress-total>0</span> Total</span>
</span>
<span class='md:hidden'>
<span data-progress-done>0</span> of <span data-progress-total>0</span> Done <span data-progress-done>0</span> of <span data-progress-total>0</span> Done
</span> </span>
</p> </p>
@ -55,11 +38,11 @@ const { hasSecondaryBanner = false, resourceId, resourceType } = Astro.props;
<ProgressShareButton <ProgressShareButton
resourceId={resourceId} resourceId={resourceId}
resourceType={resourceType} resourceType={resourceType}
client:only="react" client:only='react'
/> />
<button <button
data-popup='progress-help' data-popup='progress-help'
class='flex items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black' class='hidden items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black sm:flex'
data-progress-nums data-progress-nums
> >
<AstroIcon icon='question' /> <AstroIcon icon='question' />
@ -67,26 +50,3 @@ const { hasSecondaryBanner = false, resourceId, resourceType } = Astro.props;
</button> </button>
</div> </div>
</div> </div>
<div
data-progress-nums-container
class='striped-loader relative -mb-2 flex items-center justify-between rounded-md border bg-white px-2 py-1.5 text-sm text-gray-700 sm:hidden'
>
<span
data-progress-nums
class='text-gray-500 opacity-0 transition-opacity duration-300'
>
<span data-progress-done>0</span> of <span data-progress-total>0</span> Done
</span>
<div
class='flex items-center gap-2 opacity-0 transition-opacity duration-300'
data-progress-nums
>
<ProgressShareButton
resourceId={resourceId}
resourceType={resourceType}
client:only="react"
/>
</div>
</div>

@ -1,17 +1,17 @@
--- ---
import Icon from './AstroIcon.astro'; import {
ArrowLeftIcon,
FolderKanbanIcon,
MapIcon,
MessageCircle,
} from 'lucide-react';
import { TabLink } from './TabLink';
import LoginPopup from './AuthenticationFlow/LoginPopup.astro'; import LoginPopup from './AuthenticationFlow/LoginPopup.astro';
import RoadmapHint from './RoadmapHint.astro';
import RoadmapNote from './RoadmapNote.astro';
import TopicSearch from './TopicSearch/TopicSearch.astro';
import YouTubeAlert from './YouTubeAlert.astro';
import ProgressHelpPopup from './ProgressHelpPopup.astro'; import ProgressHelpPopup from './ProgressHelpPopup.astro';
import { MarkFavorite } from './FeaturedItems/MarkFavorite'; import { MarkFavorite } from './FeaturedItems/MarkFavorite';
import { CreateVersion } from './CreateVersion/CreateVersion';
import { type RoadmapFrontmatter } from '../lib/roadmap'; import { type RoadmapFrontmatter } from '../lib/roadmap';
import { ShareRoadmapButton } from './ShareRoadmapButton'; import { ShareRoadmapButton } from './ShareRoadmapButton';
import { Share2 } from 'lucide-react'; import { DownloadRoadmapButton } from './DownloadRoadmapButton';
import ShareIcons from './ShareIcons/ShareIcons.astro';
export interface Props { export interface Props {
title: string; title: string;
@ -24,6 +24,7 @@ export interface Props {
question?: RoadmapFrontmatter['question']; question?: RoadmapFrontmatter['question'];
hasTopics?: boolean; hasTopics?: boolean;
isForkable?: boolean; isForkable?: boolean;
activeTab?: 'roadmap' | 'projects';
} }
const { const {
@ -32,15 +33,12 @@ const {
roadmapId, roadmapId,
tnsBannerLink, tnsBannerLink,
isUpcoming = false, isUpcoming = false,
hasSearch = false,
note, note,
hasTopics = false, hasTopics = false,
question, question,
isForkable = false, activeTab = 'roadmap',
} = Astro.props; } = Astro.props;
const isRoadmapReady = !isUpcoming;
const roadmapTitle = const roadmapTitle =
roadmapId === 'devops' roadmapId === 'devops'
? 'DevOps' ? 'DevOps'
@ -52,137 +50,11 @@ const hasTnsBanner = !!tnsBannerLink;
<LoginPopup /> <LoginPopup />
<ProgressHelpPopup /> <ProgressHelpPopup />
<div class='relative border-b'> <div class='container mt-0 flex flex-col gap-2.5 px-0 sm:mt-3 sm:px-4'>
<div
class:list={[
'container relative py-5',
{
'sm:py-16': hasTnsBanner,
'sm:py-12': !hasTnsBanner,
},
]}
>
<div class='mb-3 mt-0 sm:mb-4'>
{
isForkable && (
<div class='mb-2'>
<CreateVersion client:load roadmapId={roadmapId} />
</div>
)
}
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'>
{title}
<span class='relative top-0 sm:-top-1'>
<MarkFavorite
resourceId={roadmapId}
resourceType='roadmap'
className='relative ml-1.5 text-gray-500 !opacity-100 hover:text-gray-600 focus:outline-0 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:stroke-gray-400 [&>svg]:stroke-[0.4] hover:[&>svg]:stroke-gray-600 sm:[&>svg]:h-4 sm:[&>svg]:w-4'
client:only='react'
/>
</span>
</h1>
<p class='text-sm text-gray-500 sm:text-lg'>{description}</p>
</div>
<div class='flex justify-between gap-2 sm:gap-0'>
<div class='flex gap-1 sm:gap-2'>
{
!hasSearch && (
<>
<a
href='/roadmaps'
class='rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm'
aria-label='Back to All Roadmaps'
>
&larr;<span class='hidden sm:inline'>&nbsp;All Roadmaps</span>
</a>
<ShareRoadmapButton
description={description}
pageUrl={`https://roadmap.sh/${roadmapId}`}
client:idle
/>
{isRoadmapReady && (
<>
<button
data-guest-required
data-popup='login-popup'
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm'
aria-label='Download Roadmap'
>
<Icon icon='download' />
<span class='ml-2 hidden sm:inline'>Download</span>
</button>
<a
data-auth-required
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm'
aria-label='Download Roadmap'
target='_blank'
href={`/pdfs/roadmaps/${roadmapId}.pdf`}
>
<Icon icon='download' />
<span class='ml-2 hidden sm:inline'>Download</span>
</a>
</>
)}
</>
)
}
{
hasSearch && (
<a
href={`/${roadmapId}`}
class='rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm'
aria-label='Back to Visual Roadmap'
>
&larr;
<span class='inline'>&nbsp;Visual Roadmap</span>
</a>
)
}
</div>
<div class='flex items-center gap-1 sm:gap-2'>
{
isRoadmapReady && (
<a
href={`https://github.com/kamranahmedse/developer-roadmap/issues/new/choose`}
target='_blank'
class='inline-flex items-center justify-center rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm'
aria-label='Suggest Changes'
>
<Icon icon='comment' class='h-3 w-3' />
<span class='ml-2 hidden sm:inline'>Suggest Changes</span>
<span class='ml-2 inline sm:hidden'>Suggest</span>
</a>
)
}
</div>
</div>
<!-- Desktop: Roadmap Resources - Alert -->
{
hasTopics && (
<RoadmapHint
tnsBannerLink={tnsBannerLink}
titleQuestion={question?.title}
titleAnswer={question?.description}
roadmapId={roadmapId}
/>
)
}
{hasSearch && <TopicSearch />}
</div>
{ {
tnsBannerLink && ( tnsBannerLink && (
<div class='absolute left-0 right-0 top-0 hidden border-b border-b-gray-200 px-2 py-1.5 sm:block'> <div class='hidden rounded-md border bg-white px-2 py-1.5 sm:block'>
<p class='py-0.5 text-center text-sm'> <p class='py-0.5 text-left text-sm'>
<span class='badge mr-1'>Partner</span> <span class='badge mr-1'>Partner</span>
Get the latest {roadmapTitle} news from our sister site{' '} Get the latest {roadmapTitle} news from our sister site{' '}
<a href={tnsBannerLink} target='_blank' class='font-medium underline'> <a href={tnsBannerLink} target='_blank' class='font-medium underline'>
@ -192,6 +64,76 @@ const hasTnsBanner = !!tnsBannerLink;
</div> </div>
) )
} }
</div>
{note && <RoadmapNote text={note} />} <div
class='relative rounded-none border bg-white px-5 pb-0 pt-4 sm:rounded-lg'
>
<div class='flex items-start justify-between'>
<a
class='inline-flex items-center justify-center rounded-md bg-gray-300 px-2 py-1.5 text-xs font-medium hover:bg-gray-400 sm:hidden sm:text-sm'
aria-label='Back to roadmaps'
href={'/roadmaps'}
>
<ArrowLeftIcon className='h-4 w-4' />
</a>
<a
href='/roadmaps'
class='hidden rounded-md text-sm font-medium text-gray-500 transition-all hover:-translate-x-1 hover:text-black focus:outline-0 sm:block'
aria-label='Back to All Roadmaps'
>
&larr;&nbsp;<span>&nbsp;All Roadmaps</span>
</a>
<div
class='relative right-0 top-0 flex items-center gap-1 sm:-right-2 sm:-top-0.5'
>
<MarkFavorite
resourceId={roadmapId}
resourceType='roadmap'
className='relative top-px mr-2 text-gray-500 !opacity-100 hover:text-gray-600 focus:outline-0 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:stroke-gray-400 [&>svg]:stroke-[0.4] hover:[&>svg]:stroke-gray-600 sm:[&>svg]:h-4 sm:[&>svg]:w-4'
client:only='react'
/>
<DownloadRoadmapButton roadmapId={roadmapId} client:idle />
<ShareRoadmapButton
description={description}
pageUrl={`https://roadmap.sh/${roadmapId}`}
client:idle
/>
</div>
</div>
<div class='mb-5 mt-5 sm:mb-8 sm:mt-5'>
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-3xl'>
{title}
</h1>
<p class='text-balance text-sm text-gray-500 sm:text-base'>
{description}
</p>
</div>
<div class='flex justify-between gap-2 sm:gap-0'>
<div class='relative top-px flex gap-1 sm:gap-3'>
<TabLink
url={`/${roadmapId}`}
icon={MapIcon}
isActive={activeTab === 'roadmap'}
text='Roadmap'
/>
<TabLink
url={`/${roadmapId}/projects`}
icon={FolderKanbanIcon}
text='Projects'
isActive={activeTab === 'projects'}
badgeText='soon'
/>
</div>
<TabLink
url={`https://github.com/kamranahmedse/developer-roadmap/issues/new/choose`}
icon={MessageCircle}
text='Suggest Changes'
isExternal={true}
hideTextOnMobile={true}
/>
</div>
</div>
</div>

@ -1,7 +1,6 @@
--- ---
import AstroIcon from './AstroIcon.astro'; import AstroIcon from './AstroIcon.astro';
import Icon from './AstroIcon.astro'; import Icon from './AstroIcon.astro';
import { RoadmapTitleQuestion } from './RoadmapTitleQuestion.tsx';
import ResourceProgressStats from './ResourceProgressStats.astro'; import ResourceProgressStats from './ResourceProgressStats.astro';
export interface Props { export interface Props {
@ -11,47 +10,13 @@ export interface Props {
titleAnswer?: string; titleAnswer?: string;
} }
const { const { roadmapId, tnsBannerLink } = Astro.props;
roadmapId,
titleQuestion = '',
titleAnswer = '',
tnsBannerLink,
} = Astro.props;
const hasTitleQuestion = titleQuestion && titleAnswer;
const hasTnsBanner = !!tnsBannerLink; const hasTnsBanner = !!tnsBannerLink;
--- ---
<div <div class:list={['mb-0 rounded-md border mt-2 bg-white']}>
class:list={[
'mb-0 mt-4 rounded-md border-0 bg-white sm:mt-7 sm:border',
...(hasTnsBanner
? [
{
'sm:-mb-[110px]': hasTitleQuestion,
'sm:-mb-[81px]': !hasTitleQuestion,
},
]
: [
{
'sm:-mb-[88px]': hasTitleQuestion,
'sm:-mb-[65px]': !hasTitleQuestion,
},
]),
]}
>
<ResourceProgressStats <ResourceProgressStats
resourceId={roadmapId} resourceId={roadmapId}
resourceType='roadmap' resourceType='roadmap'
hasSecondaryBanner={Boolean(hasTitleQuestion)}
/> />
{
hasTitleQuestion && (
<RoadmapTitleQuestion
client:load
question={titleQuestion}
answer={titleAnswer}
/>
)
}
</div> </div>

@ -1,4 +1,10 @@
import { ChevronDown, ChevronUp, GraduationCap } from 'lucide-react'; import {
ChevronDown,
ChevronUp,
CircleHelp,
GraduationCap,
Info,
} from 'lucide-react';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useOutsideClick } from '../hooks/use-outside-click'; import { useOutsideClick } from '../hooks/use-outside-click';
import { markdownToHtml } from '../lib/markdown'; import { markdownToHtml } from '../lib/markdown';
@ -19,23 +25,23 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
}); });
return ( return (
<div className="relative hidden border-t text-sm font-medium sm:block"> <div className="relative hidden rounded-b-[5px] border-t bg-white text-sm font-medium hover:bg-gray-50 sm:block">
{isAnswerVisible && ( {isAnswerVisible && (
<div className="fixed left-0 right-0 top-0 z-[100] h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"></div> <div className="fixed left-0 right-0 top-0 z-[100] h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"></div>
)} )}
<h2 <h2
className="z-50 flex cursor-pointer items-center px-2 py-2.5 text-base font-medium" className="z-50 flex cursor-pointer select-none items-center px-2 py-2 text-sm font-medium"
aria-expanded={isAnswerVisible ? 'true' : 'false'} aria-expanded={isAnswerVisible ? 'true' : 'false'}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setIsAnswerVisible(!isAnswerVisible); setIsAnswerVisible(!isAnswerVisible);
}} }}
> >
<span className="flex flex-grow items-center"> <span className="flex flex-grow select-none items-center">
<GraduationCap className="mr-2 inline-block h-6 w-6" /> <Info className="mr-1.5 inline-block h-4 w-4" strokeWidth={2.5} />
{question} {question}
</span> </span>
<span className="flex-shrink-0 text-gray-400"> <span className="relative -top-px flex-shrink-0 text-gray-400">
<ChevronDown className={`inline-block h-5 w-5`} /> <ChevronDown className={`inline-block h-5 w-5`} />
</span> </span>
</h2> </h2>
@ -48,14 +54,14 @@ export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
> >
{isAnswerVisible && ( {isAnswerVisible && (
<h2 <h2
className="flex cursor-pointer items-center border-b px-[7px] py-[9px] text-base font-medium" className="flex cursor-pointer select-none items-center border-b px-[7px] py-[9px] text-base font-medium"
onClick={() => setIsAnswerVisible(false)} onClick={() => setIsAnswerVisible(false)}
> >
<span className="flex flex-grow items-center"> <span className="flex flex-grow items-center">
<GraduationCap className="mr-2 inline-block h-6 w-6" /> <Info className="mr-2 inline-block h-4 w-4" strokeWidth={2.5} />
{question} {question}
</span> </span>
<span className="flex-shrink-0 text-gray-400"> <span className="relative -top-px flex-shrink-0 text-gray-400">
<ChevronUp className={`inline-block h-5 w-5`} /> <ChevronUp className={`inline-block h-5 w-5`} />
</span> </span>
</h2> </h2>

@ -70,7 +70,7 @@ export function ShareRoadmapButton(props: ShareRoadmapButtonProps) {
</button> </button>
{isDropdownOpen && ( {isDropdownOpen && (
<div className="absolute left-0 z-[999] mt-1 w-48 rounded-md bg-slate-800 text-sm text-white shadow-lg ring-1 ring-black ring-opacity-5"> <div className="absolute right-0 z-[999] mt-1 w-40 rounded-md bg-slate-800 text-sm text-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="flex flex-col px-1 py-1"> <div className="flex flex-col px-1 py-1">
<button <button
onClick={() => { onClick={() => {

@ -0,0 +1,69 @@
import type { LucideIcon } from 'lucide-react';
import { cn } from '../lib/classname.ts';
type TabLinkProps = {
icon: LucideIcon;
text: string;
isActive: boolean;
isExternal?: boolean;
badgeText?: string;
hideTextOnMobile?: boolean;
url: string;
};
export function TabLink(props: TabLinkProps) {
const {
icon: Icon,
badgeText,
isExternal = false,
url,
text,
isActive,
hideTextOnMobile = false,
} = props;
const className = cn(
'inline-flex group transition-colors items-center gap-1.5 border-b-2 px-2 pb-2.5 text-sm',
{
'cursor-default border-b-black font-medium text-black': isActive,
'border-b-transparent font-normal text-gray-400 hover:text-gray-700':
!isActive,
'font-medium hover:text-black text-gray-500 px-0': isExternal,
},
);
const textClass = cn({
'hidden sm:inline': hideTextOnMobile,
});
const badgeNode = badgeText && (
<span className="ml-0.5 hidden items-center gap-0.5 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-black transition-colors group-hover:bg-yellow-300 sm:flex">
<span className="relative -top-px">{badgeText}</span>
</span>
);
if (isActive) {
return (
<span className={className}>
<Icon className="h-4 w-4 flex-shrink-0" />
<span className={textClass}>{text}</span>
{badgeNode}
</span>
);
}
return (
<a
target={isExternal ? '_blank' : undefined}
onClick={(e) => {
e.preventDefault();
}}
href={url}
className={className}
>
<Icon className="h-4 w-4 flex-shrink-0" />
<span className={textClass}>{text}</span>
{badgeNode}
</a>
);
}

@ -8,6 +8,14 @@ title: 'System Design'
description: 'Everything you need to know about designing large scale systems.' description: 'Everything you need to know about designing large scale systems.'
isNew: false isNew: false
hasTopics: true hasTopics: true
question:
title: 'What is System Design?'
description: |
System design involves creating a detailed blueprint of a system's architecture, components, modules, interfaces, and data to fulfill specific requirements. It includes outlining a structured plan for building, implementing, and maintaining the system, ensuring it meets functional, technical, and business needs. This process addresses considerations of scalability, performance, security, and usability, aiming to develop an efficient and effective solution.
## What are the components of System Design?
Some of the the major components that play a crucial role in designing a system include Programming language choice, Databases, CDNs, Load Balancers, Caches, Proxies, Queues, Web Servers, Application Servers, Search Engines, Logging and Monitoring Systems, Scaling, and more. Key considerations include scalability, architectural patterns, and security measures to safeguard the system. These elements collectively contribute to building a robust, efficient, and secure system, though this list represents just a subset of the comprehensive factors involved in system design.
dimensions: dimensions:
width: 968 width: 968
height: 2848.5 height: 2848.5

@ -451,9 +451,9 @@ export function refreshProgressCounters() {
const doneCountEls = document.querySelectorAll('[data-progress-done]'); const doneCountEls = document.querySelectorAll('[data-progress-done]');
if (doneCountEls.length > 0) { if (doneCountEls.length > 0) {
doneCountEls.forEach( doneCountEls.forEach((doneCountEl) => {
(doneCountEl) => (doneCountEl.innerHTML = `${totalDone}`), doneCountEl.innerHTML = `${totalDone + totalSkipped}`;
); });
} }
const learningCountEls = document.querySelectorAll( const learningCountEls = document.querySelectorAll(

@ -6,7 +6,6 @@ import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro';
import RoadmapHeader from '../../components/RoadmapHeader.astro'; import RoadmapHeader from '../../components/RoadmapHeader.astro';
import ShareIcons from '../../components/ShareIcons/ShareIcons.astro'; import ShareIcons from '../../components/ShareIcons/ShareIcons.astro';
import { TopicDetail } from '../../components/TopicDetail/TopicDetail'; import { TopicDetail } from '../../components/TopicDetail/TopicDetail';
import UpcomingForm from '../../components/UpcomingForm.astro';
import { UserProgressModal } from '../../components/UserProgress/UserProgressModal'; import { UserProgressModal } from '../../components/UserProgress/UserProgressModal';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { import {
@ -15,6 +14,9 @@ import {
} from '../../lib/jsonld-schema'; } from '../../lib/jsonld-schema';
import { getOpenGraphImageUrl } from '../../lib/open-graph'; import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
import RoadmapNote from '../../components/RoadmapNote.astro';
import { RoadmapTitleQuestion } from '../../components/RoadmapTitleQuestion';
import ResourceProgressStats from '../../components/ResourceProgressStats.astro';
export async function getStaticPaths() { export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds(); const roadmapIds = await getRoadmapIds();
@ -63,6 +65,9 @@ const ogImageUrl =
group: 'roadmap', group: 'roadmap',
resourceId: roadmapId, resourceId: roadmapId,
}); });
const question = roadmapData?.question;
const note = roadmapData.note;
--- ---
<BaseLayout <BaseLayout
@ -87,52 +92,65 @@ const ogImageUrl =
slot='after-header' slot='after-header'
/> />
<RoadmapHeader <TopicDetail
title={roadmapData.title} resourceTitle={roadmapData.title}
description={roadmapData.description} resourceType='roadmap'
note={roadmapData.note} client:idle
tnsBannerLink={roadmapData.tnsBannerLink} canSubmitContribution={true}
roadmapId={roadmapId}
hasTopics={roadmapData.hasTopics}
isUpcoming={roadmapData.isUpcoming}
isForkable={roadmapData.isForkable}
question={roadmapData.question}
/> />
<div class='bg-gray-50 pt-4 sm:pt-12'> <div class='bg-gray-50'>
{ <RoadmapHeader
!roadmapData.isUpcoming && ( title={roadmapData.title}
<div class='container relative !max-w-[1000px]'> description={roadmapData.description}
<ShareIcons note={roadmapData.note}
description={roadmapData.briefDescription} tnsBannerLink={roadmapData.tnsBannerLink}
pageUrl={`https://roadmap.sh/${roadmapId}`} roadmapId={roadmapId}
/> hasTopics={roadmapData.hasTopics}
<TopicDetail isUpcoming={roadmapData.isUpcoming}
resourceTitle={roadmapData.title} isForkable={roadmapData.isForkable}
resourceType='roadmap' question={roadmapData.question}
client:idle />
canSubmitContribution={true}
/>
{roadmapData?.renderer === 'editor' ? ( <div class='container mt-2.5'>
<EditorRoadmap <div class='rounded-md border bg-white'>
resourceId={roadmapId} <ResourceProgressStats resourceId={roadmapId} resourceType='roadmap' />
resourceType='roadmap' {
dimensions={roadmapData.dimensions!} question?.title && (
<RoadmapTitleQuestion
client:load client:load
question={question?.title}
answer={question?.description}
/> />
) : ( )
<FrameRenderer }
resourceType={'roadmap'} </div>
resourceId={roadmapId} </div>
dimensions={roadmapData.dimensions}
/> <div class='container relative !max-w-[1000px]'>
)} <ShareIcons
</div> description={roadmapData.briefDescription}
) pageUrl={`https://roadmap.sh/${roadmapId}`}
} />
{
roadmapData?.renderer === 'editor' ? (
<EditorRoadmap
resourceId={roadmapId}
resourceType='roadmap'
dimensions={roadmapData.dimensions!}
client:load
/>
) : (
<FrameRenderer
resourceType={'roadmap'}
resourceId={roadmapId}
dimensions={roadmapData.dimensions}
/>
)
}
</div>
{roadmapData.isUpcoming && <UpcomingForm />}
<UserProgressModal <UserProgressModal
resourceId={roadmapId} resourceId={roadmapId}
resourceType='roadmap' resourceType='roadmap'

@ -0,0 +1,84 @@
---
import { EditorRoadmap } from '../../components/EditorRoadmap/EditorRoadmap';
import FAQs, { type FAQType } from '../../components/FAQs/FAQs.astro';
import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro';
import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro';
import RoadmapHeader from '../../components/RoadmapHeader.astro';
import { FolderKanbanIcon } from 'lucide-react';
import { EmptyProjects } from '../../components/Projects/EmptyProjects';
import ShareIcons from '../../components/ShareIcons/ShareIcons.astro';
import { TopicDetail } from '../../components/TopicDetail/TopicDetail';
import { UserProgressModal } from '../../components/UserProgress/UserProgressModal';
import BaseLayout from '../../layouts/BaseLayout.astro';
import {
generateArticleSchema,
generateFAQSchema,
} from '../../lib/jsonld-schema';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
import RoadmapNote from '../../components/RoadmapNote.astro';
import { RoadmapTitleQuestion } from '../../components/RoadmapTitleQuestion';
import ResourceProgressStats from '../../components/ResourceProgressStats.astro';
export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds();
return roadmapIds.map((roadmapId) => ({
params: { roadmapId },
}));
}
interface Params extends Record<string, string | undefined> {
roadmapId: string;
}
const { roadmapId } = Astro.params as Params;
const roadmapFile = await import(
`../../data/roadmaps/${roadmapId}/${roadmapId}.md`
);
const roadmapData = roadmapFile.frontmatter as RoadmapFrontmatter;
// update og for projects
const ogImageUrl =
roadmapData?.seo?.ogImageUrl ||
getOpenGraphImageUrl({
group: 'roadmap',
resourceId: roadmapId,
});
---
<BaseLayout
permalink={`/${roadmapId}`}
title={roadmapData?.seo?.title}
briefTitle={roadmapData.briefTitle}
ogImageUrl={ogImageUrl}
description={roadmapData.seo.description}
keywords={roadmapData.seo.keywords}
noIndex={true}
resourceId={roadmapId}
resourceType='roadmap'
>
<div class='bg-gray-50'>
<RoadmapHeader
title={roadmapData.title}
description={roadmapData.description}
note={roadmapData.note}
tnsBannerLink={roadmapData.tnsBannerLink}
roadmapId={roadmapId}
hasTopics={roadmapData.hasTopics}
isUpcoming={roadmapData.isUpcoming}
isForkable={roadmapData.isForkable}
question={roadmapData.question}
activeTab='projects'
/>
<div class='container'>
<div
class='relative my-2.5 flex min-h-[400px] flex-col items-center justify-center rounded-lg border bg-white'
>
<EmptyProjects client:load />
</div>
</div>
</div>
</BaseLayout>

@ -7,6 +7,10 @@
@apply mx-auto !max-w-[830px] px-4; @apply mx-auto !max-w-[830px] px-4;
} }
.container-lg {
@apply mx-auto !max-w-[968px] px-4;
}
.badge { .badge {
@apply rounded-sm bg-gray-400 px-1.5 py-0.5 text-xs font-medium uppercase text-white; @apply rounded-sm bg-gray-400 px-1.5 py-0.5 text-xs font-medium uppercase text-white;
} }

Loading…
Cancel
Save