parent
852622f5ac
commit
60568caff7
10 changed files with 209 additions and 38 deletions
@ -0,0 +1,61 @@ |
||||
--- |
||||
import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide'; |
||||
import MarkdownFile from '../MarkdownFile.astro'; |
||||
import { TableOfContent } from '../TableOfContent/TableOfContent'; |
||||
|
||||
interface Props { |
||||
guide: GuideFileType; |
||||
} |
||||
|
||||
const { guide } = Astro.props; |
||||
|
||||
const allHeadings = guide.getHeadings(); |
||||
const tableOfContent = getGuideTableOfContent(allHeadings); |
||||
|
||||
const showTableOfContent = tableOfContent.length > 0; |
||||
const { frontmatter: guideFrontmatter, author } = guide; |
||||
--- |
||||
|
||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'> |
||||
{ |
||||
showTableOfContent && ( |
||||
<div class='bg-gradient-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'> |
||||
<TableOfContent toc={tableOfContent} client:load /> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
<div |
||||
class:list={['col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10', { |
||||
'lg:border-r': showTableOfContent |
||||
}]} |
||||
> |
||||
<MarkdownFile> |
||||
<h1 class='text-balance text-4xl mb-3 font-bold'>{guideFrontmatter.title}</h1> |
||||
<p |
||||
class='flex items-center justify-start text-sm text-gray-400 my-0' |
||||
> |
||||
<a |
||||
href={`/authors/${author.id}`} |
||||
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline underline-offset-2' |
||||
> |
||||
<img |
||||
alt={author.frontmatter.name} |
||||
src={author.frontmatter.imageUrl} |
||||
class='mb-0 mr-2 inline h-5 w-5 rounded-full' |
||||
/> |
||||
{author.frontmatter.name} |
||||
</a> |
||||
<span class='mx-2 hidden sm:inline'>·</span> |
||||
<a |
||||
class='hover:text-gray-600 underline-offset-2 hidden sm:inline' |
||||
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`} |
||||
target='_blank' |
||||
> |
||||
Improve this Guide |
||||
</a> |
||||
</p> |
||||
<guide.Content /> |
||||
</MarkdownFile> |
||||
</div> |
||||
</article> |
@ -1,5 +1,5 @@ |
||||
<div |
||||
class='prose-xl prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-10 prose-h2:mb-3 prose-h5:font-medium prose-h3:mt-2 prose-img:mt-1' |
||||
class='container prose-h2:text-balance prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-3xl prose-h3:mt-2 prose-h3:scroll-mt-5 prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 prose-h2:sm:scroll-mt-10 prose-h3:sm:scroll-mt-10' |
||||
> |
||||
<slot /> |
||||
</div> |
||||
|
@ -0,0 +1,98 @@ |
||||
import { useState, type CSSProperties } from 'react'; |
||||
import type { HeadingGroupType } from '../../lib/guide'; |
||||
import { ChevronDown } from 'lucide-react'; |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type TableOfContentProps = { |
||||
toc: HeadingGroupType[]; |
||||
}; |
||||
|
||||
export function TableOfContent(props: TableOfContentProps) { |
||||
const { toc } = props; |
||||
|
||||
const [isOpen, setIsOpen] = useState(false); |
||||
|
||||
if (toc.length === 0) { |
||||
return null; |
||||
} |
||||
|
||||
const totalRows = toc.flatMap((heading) => { |
||||
return [heading, ...heading.children]; |
||||
}).length; |
||||
|
||||
return ( |
||||
<div |
||||
className={cn( |
||||
'relative min-w-[250px] px-5 pt-0 max-lg:min-w-full max-lg:max-w-full max-lg:border-none max-lg:px-0 lg:pt-10', |
||||
{ |
||||
'top-0 lg:sticky': totalRows <= 20, |
||||
}, |
||||
)} |
||||
> |
||||
<h4 className="text-lg font-medium max-lg:hidden">In this article</h4> |
||||
<button |
||||
className="flex w-full items-center justify-between gap-2 bg-gray-300 px-3 py-2 text-sm font-medium lg:hidden" |
||||
onClick={() => setIsOpen(!isOpen)} |
||||
> |
||||
Table of Contents |
||||
<ChevronDown |
||||
size={16} |
||||
className={cn( |
||||
'transform transition-transform', |
||||
isOpen && 'rotate-180', |
||||
)} |
||||
/> |
||||
</button> |
||||
|
||||
<ol |
||||
className={cn( |
||||
'mt-2 space-y-1 max-lg:absolute max-lg:top-full max-lg:mt-0 max-lg:w-full max-lg:space-y-0 max-lg:bg-white max-lg:shadow', |
||||
!isOpen && 'hidden lg:block', |
||||
isOpen && 'block', |
||||
)} |
||||
> |
||||
{toc.map((heading) => ( |
||||
<li key={heading.slug}> |
||||
<a |
||||
href={`#${heading.slug}`} |
||||
className="text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1" |
||||
onClick={() => { |
||||
if (!isOpen) { |
||||
return; |
||||
} |
||||
|
||||
setIsOpen(false); |
||||
}} |
||||
> |
||||
{heading.text} |
||||
</a> |
||||
|
||||
{heading.children.length > 0 && ( |
||||
<ol className="my-0 ml-4 mt-1 space-y-1 max-lg:ml-0 max-lg:mt-0 max-lg:list-none max-lg:space-y-0"> |
||||
{heading.children.map((children) => { |
||||
return ( |
||||
<li key={children.slug}> |
||||
<a |
||||
href={`#${children.slug}`} |
||||
className="text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1 max-lg:pl-8" |
||||
onClick={() => { |
||||
if (!isOpen) { |
||||
return; |
||||
} |
||||
|
||||
setIsOpen(false); |
||||
}} |
||||
> |
||||
{children.text} |
||||
</a> |
||||
</li> |
||||
); |
||||
})} |
||||
</ol> |
||||
)} |
||||
</li> |
||||
))} |
||||
</ol> |
||||
</div> |
||||
); |
||||
} |
Loading…
Reference in new issue