mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-01-16 21:58:30 +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
|
.idea
|
||||||
.temp
|
.temp
|
||||||
|
.astro
|
||||||
|
|
||||||
# build output
|
# build output
|
||||||
dist/
|
dist/
|
||||||
|
@ -3,6 +3,7 @@ import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide';
|
|||||||
import MarkdownFile from '../MarkdownFile.astro';
|
import MarkdownFile from '../MarkdownFile.astro';
|
||||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||||
import { replaceVariables } from '../../lib/markdown';
|
import { replaceVariables } from '../../lib/markdown';
|
||||||
|
import { RelatedGuides } from './RelatedGuides';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
guide: GuideFileType;
|
guide: GuideFileType;
|
||||||
@ -14,13 +15,21 @@ const allHeadings = guide.getHeadings();
|
|||||||
const tableOfContent = getGuideTableOfContent(allHeadings);
|
const tableOfContent = getGuideTableOfContent(allHeadings);
|
||||||
|
|
||||||
const showTableOfContent = tableOfContent.length > 0;
|
const showTableOfContent = tableOfContent.length > 0;
|
||||||
|
const showRelatedGuides =
|
||||||
|
guide?.frontmatter?.relatedGuides &&
|
||||||
|
Object.keys(guide?.frontmatter?.relatedGuides).length > 0;
|
||||||
const { frontmatter: guideFrontmatter, author } = guide;
|
const { frontmatter: guideFrontmatter, author } = guide;
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
|
<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'>
|
<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 />
|
<TableOfContent toc={tableOfContent} client:load />
|
||||||
</div>
|
</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;
|
priority: number;
|
||||||
changefreq: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
changefreq: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||||
};
|
};
|
||||||
|
relatedTitle?: string;
|
||||||
|
relatedGuides?: Record<string, string>;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,5 +118,11 @@ export function getGuideTableOfContent(headings: HeadingType[]) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (tableOfContents.length > 5) {
|
||||||
|
tableOfContents.forEach((group) => {
|
||||||
|
group.children = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return tableOfContents;
|
return tableOfContents;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export function replaceVariables(
|
|||||||
currentYear: new Date().getFullYear().toString(),
|
currentYear: new Date().getFullYear().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return markdown.replace(/@([^@]+)@/g, (match, p1) => {
|
return markdown?.replace(/@([^@]+)@/g, (match, p1) => {
|
||||||
return allVariables[p1] || match;
|
return allVariables[p1] || match;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ const guide = await getGuideById(guideId);
|
|||||||
const { frontmatter: guideData } = guide!;
|
const { frontmatter: guideData } = guide!;
|
||||||
|
|
||||||
const ogImageUrl =
|
const ogImageUrl =
|
||||||
guideData.seo.ogImageUrl ||
|
guideData.seo?.ogImageUrl ||
|
||||||
getOpenGraphImageUrl({
|
getOpenGraphImageUrl({
|
||||||
group: 'guide',
|
group: 'guide',
|
||||||
resourceId: guideId,
|
resourceId: guideId,
|
||||||
@ -20,9 +20,9 @@ const ogImageUrl =
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title={replaceVariables(guideData.seo.title)}
|
title={replaceVariables(guideData?.seo?.title)}
|
||||||
description={replaceVariables(guideData.seo.description)}
|
description={replaceVariables(guideData?.seo?.description)}
|
||||||
permalink={guide.frontmatter.excludedBySlug}
|
permalink={guideData.excludedBySlug}
|
||||||
canonicalUrl={guideData.canonicalUrl}
|
canonicalUrl={guideData.canonicalUrl}
|
||||||
ogImageUrl={ogImageUrl}
|
ogImageUrl={ogImageUrl}
|
||||||
>
|
>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user