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 formattingpull/6348/head
parent
5a052d0db2
commit
1087e1a935
16 changed files with 447 additions and 286 deletions
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
Loading…
Reference in new issue