mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-02 13:52:46 +02:00
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
This commit is contained in:
40
src/components/DownloadRoadmapButton.tsx
Normal file
40
src/components/DownloadRoadmapButton.tsx
Normal file
@@ -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>
|
||||||
|
60
src/components/Projects/EmptyProjects.tsx
Normal file
60
src/components/Projects/EmptyProjects.tsx
Normal file
@@ -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'>·</span
|
|
||||||
>
|
|
||||||
<span><span data-progress-learning>0</span> in progress</span><span
|
|
||||||
class='mx-1.5 text-gray-400'>·</span
|
|
||||||
>
|
|
||||||
<span><span data-progress-skipped>0</span> skipped</span><span
|
|
||||||
class='mx-1.5 text-gray-400'>·</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'
|
|
||||||
>
|
|
||||||
←<span class='hidden sm:inline'> 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'
|
|
||||||
>
|
|
||||||
←
|
|
||||||
<span class='inline'> 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'
|
||||||
|
>
|
||||||
|
← <span> 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={() => {
|
||||||
|
69
src/components/TabLink.tsx
Normal file
69
src/components/TabLink.tsx
Normal file
@@ -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'
|
||||||
|
84
src/pages/[roadmapId]/projects.astro
Normal file
84
src/pages/[roadmapId]/projects.astro
Normal file
@@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user