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