mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-01-16 13:51:23 +01:00
feat: related guides sidebar (#7682)
* feat: related guides sidebar * fix: hide related guides on mobile
This commit is contained in:
parent
a0addd1408
commit
f47bf798d3
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
.idea
|
||||
.temp
|
||||
.astro
|
||||
|
||||
# build output
|
||||
dist/
|
||||
|
@ -3,6 +3,7 @@ import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide';
|
||||
import MarkdownFile from '../MarkdownFile.astro';
|
||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||
import { replaceVariables } from '../../lib/markdown';
|
||||
import { RelatedGuides } from './RelatedGuides';
|
||||
|
||||
interface Props {
|
||||
guide: GuideFileType;
|
||||
@ -14,13 +15,21 @@ const allHeadings = guide.getHeadings();
|
||||
const tableOfContent = getGuideTableOfContent(allHeadings);
|
||||
|
||||
const showTableOfContent = tableOfContent.length > 0;
|
||||
const showRelatedGuides =
|
||||
guide?.frontmatter?.relatedGuides &&
|
||||
Object.keys(guide?.frontmatter?.relatedGuides).length > 0;
|
||||
const { frontmatter: guideFrontmatter, author } = guide;
|
||||
---
|
||||
|
||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
|
||||
{
|
||||
showTableOfContent && (
|
||||
(showTableOfContent || showRelatedGuides) && (
|
||||
<div class='bg-gradient-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
|
||||
<RelatedGuides
|
||||
relatedTitle={guideFrontmatter?.relatedTitle}
|
||||
relatedGuides={guideFrontmatter?.relatedGuides || {}}
|
||||
client:load
|
||||
/>
|
||||
<TableOfContent toc={tableOfContent} client:load />
|
||||
</div>
|
||||
)
|
||||
|
74
src/components/Guide/RelatedGuides.tsx
Normal file
74
src/components/Guide/RelatedGuides.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
type RelatedGuidesProps = {
|
||||
relatedTitle?: string;
|
||||
relatedGuides: Record<string, string>;
|
||||
};
|
||||
|
||||
export function RelatedGuides(props: RelatedGuidesProps) {
|
||||
const { relatedTitle = 'Other Guides', relatedGuides } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const relatedGuidesArray = Object.entries(relatedGuides).map(
|
||||
([title, url]) => ({
|
||||
title,
|
||||
url,
|
||||
}),
|
||||
);
|
||||
|
||||
if (relatedGuidesArray.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative -mb-5 min-w-[250px] px-5 pt-0 max-lg:hidden lg:pt-10',
|
||||
)}
|
||||
>
|
||||
<h4 className="text-lg font-medium max-lg:hidden">{relatedTitle}</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)}
|
||||
>
|
||||
{relatedTitle}
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn(
|
||||
'transform transition-transform',
|
||||
isOpen && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<ol
|
||||
className={cn(
|
||||
'mt-0.5 space-y-0 max-lg:absolute max-lg:top-full max-lg:z-10 max-lg:mt-0 max-lg:w-full max-lg:bg-white max-lg:shadow',
|
||||
!isOpen && 'hidden lg:block',
|
||||
isOpen && 'block',
|
||||
)}
|
||||
>
|
||||
{relatedGuidesArray.map((relatedGuide) => (
|
||||
<li key={relatedGuide.url}>
|
||||
<a
|
||||
href={relatedGuide.url}
|
||||
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);
|
||||
}}
|
||||
>
|
||||
{relatedGuide.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -20,6 +20,8 @@ export interface GuideFrontmatter {
|
||||
priority: number;
|
||||
changefreq: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||
};
|
||||
relatedTitle?: string;
|
||||
relatedGuides?: Record<string, string>;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
@ -116,5 +118,11 @@ export function getGuideTableOfContent(headings: HeadingType[]) {
|
||||
}
|
||||
});
|
||||
|
||||
if (tableOfContents.length > 5) {
|
||||
tableOfContents.forEach((group) => {
|
||||
group.children = [];
|
||||
});
|
||||
}
|
||||
|
||||
return tableOfContents;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ export function replaceVariables(
|
||||
currentYear: new Date().getFullYear().toString(),
|
||||
};
|
||||
|
||||
return markdown.replace(/@([^@]+)@/g, (match, p1) => {
|
||||
return markdown?.replace(/@([^@]+)@/g, (match, p1) => {
|
||||
return allVariables[p1] || match;
|
||||
});
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ const guide = await getGuideById(guideId);
|
||||
const { frontmatter: guideData } = guide!;
|
||||
|
||||
const ogImageUrl =
|
||||
guideData.seo.ogImageUrl ||
|
||||
guideData.seo?.ogImageUrl ||
|
||||
getOpenGraphImageUrl({
|
||||
group: 'guide',
|
||||
resourceId: guideId,
|
||||
@ -20,9 +20,9 @@ const ogImageUrl =
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={replaceVariables(guideData.seo.title)}
|
||||
description={replaceVariables(guideData.seo.description)}
|
||||
permalink={guide.frontmatter.excludedBySlug}
|
||||
title={replaceVariables(guideData?.seo?.title)}
|
||||
description={replaceVariables(guideData?.seo?.description)}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
>
|
||||
|
Loading…
x
Reference in New Issue
Block a user