-
Notifications
You must be signed in to change notification settings - Fork 816
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #386 from mfts/feat/help-center
feat: add help articles
- Loading branch information
Showing
14 changed files
with
393 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { getHelpArticles, getHelpArticle } from "@/lib/content/help"; | ||
import { ContentBody } from "@/components/mdx/post-body"; | ||
import { notFound } from "next/navigation"; | ||
import Link from "next/link"; | ||
import BlurImage from "@/components/blur-image"; | ||
import { constructMetadata, formatDate } from "@/lib/utils"; | ||
import { Metadata } from "next"; | ||
import { | ||
Breadcrumb, | ||
BreadcrumbItem, | ||
BreadcrumbLink, | ||
BreadcrumbList, | ||
BreadcrumbPage, | ||
BreadcrumbSeparator, | ||
} from "@/components/ui/breadcrumb"; | ||
import TableOfContents from "@/components/mdx/table-of-contents"; | ||
|
||
export async function generateStaticParams() { | ||
const articles = await getHelpArticles(); | ||
return articles.map((article) => ({ slug: article?.data.slug })); | ||
} | ||
|
||
export const generateMetadata = async ({ | ||
params, | ||
}: { | ||
params: { | ||
slug: string; | ||
}; | ||
}): Promise<Metadata> => { | ||
const article = (await getHelpArticles()).find( | ||
(article) => article?.data.slug === params.slug, | ||
); | ||
const { title, summary: description, image } = article?.data || {}; | ||
|
||
return constructMetadata({ | ||
title: `${title} - Papermark`, | ||
description, | ||
image, | ||
}); | ||
}; | ||
|
||
export default async function BlogPage({ | ||
params, | ||
}: { | ||
params: { slug: string }; | ||
}) { | ||
const article = await getHelpArticle(params.slug); | ||
if (!article) return notFound(); | ||
|
||
// const category = article.data.categories ? article.data.categories[0] : ""; | ||
|
||
return ( | ||
<> | ||
<div className="max-w-7xl w-full mx-auto px-4 md:px-8 mb-10"> | ||
<div className="flex max-w-screen-sm flex-col space-y-4 pt-16"> | ||
<div className="flex items-center space-x-4"> | ||
<Breadcrumb> | ||
<BreadcrumbList> | ||
<BreadcrumbItem> | ||
<BreadcrumbPage>Help Center</BreadcrumbPage> | ||
</BreadcrumbItem> | ||
{/* <BreadcrumbSeparator /> | ||
<BreadcrumbItem> | ||
<BreadcrumbLink asChild> | ||
<Link href={`/help/category/${category}`}>{category}</Link> | ||
</BreadcrumbLink> | ||
</BreadcrumbItem> */} | ||
<BreadcrumbSeparator /> | ||
<BreadcrumbItem> | ||
<BreadcrumbPage>{article.data.title}</BreadcrumbPage> | ||
</BreadcrumbItem> | ||
</BreadcrumbList> | ||
</Breadcrumb> | ||
</div> | ||
<h1 className="text-4xl md:text-5xl text-balance"> | ||
{article.data.title} | ||
</h1> | ||
<p className="text-lg text-gray-600">{article.data.summary}</p> | ||
|
||
<div className="items-center space-x-4 flex flex-col self-start"> | ||
<Link | ||
href={`https://twitter.com/mfts0`} | ||
className="group flex items-center space-x-3" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<BlurImage | ||
src={`https://pbs.twimg.com/profile_images/1176854646343852032/iYnUXJ-m_400x400.jpg`} | ||
alt={`Marc Seitz`} | ||
width={40} | ||
height={40} | ||
className="rounded-full transition-all group-hover:brightness-90" | ||
/> | ||
<div className="flex flex-col"> | ||
<p className="font-semibold text-gray-700">Marc Seitz</p> | ||
<p className="text-sm text-gray-500">@mfts0</p> | ||
</div> | ||
</Link> | ||
</div> | ||
<div className="flex items-center space-x-4 text-gray-700"> | ||
<time dateTime={article.data.publishedAt} className="text-sm"> | ||
Last updated {formatDate(article.data.publishedAt, true)} | ||
</time> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div className="relative"> | ||
<div className="grid grid-cols-4 gap-10 py-10 max-w-7xl w-full mx-auto px-4 md:px-8"> | ||
<div className="relative col-span-4 mb-10 flex flex-col space-y-8 bg-white md:col-span-3 sm:border-r sm:border-orange-500"> | ||
<div | ||
data-mdx-container | ||
className="prose prose-h2:mb-2 first:prose-h2:mt-0 prose-h2:mt-10 prose-headings:font-medium sm:max-w-screen-md sm:pr-2 md:pr-0" | ||
> | ||
<ContentBody>{article.body}</ContentBody> | ||
</div> | ||
</div> | ||
|
||
<div className="sticky top-14 col-span-1 hidden flex-col divide-y divide-gray-200 self-start sm:flex"> | ||
<div className="flex flex-col space-y-4"> | ||
<TableOfContents items={article.toc} /> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
"use client"; | ||
|
||
// import useCurrentAnchor from "#/lib/hooks/use-current-anchor"; | ||
import { cn } from "@/lib/utils"; | ||
import slugify from "@sindresorhus/slugify"; | ||
import Link from "next/link"; | ||
|
||
export default function TableOfContents({ | ||
items, | ||
}: { | ||
items: { | ||
text: string; | ||
level: number; | ||
}[]; | ||
}) { | ||
const currentAnchor = useCurrentAnchor(); | ||
|
||
return ( | ||
<div className="grid gap-4 -ml-[2.55rem] pl-10 border-l border-orange-500"> | ||
{items && | ||
items.map((item, idx) => { | ||
const itemId = slugify(item.text); | ||
return ( | ||
<Link | ||
key={itemId} | ||
href={`#${itemId}`} | ||
className={cn("text-sm text-gray-500 ", { | ||
"border-l-2 border-black -ml-[2.6rem] pl-10 text-black": | ||
currentAnchor ? currentAnchor === itemId : idx === 0, | ||
})} | ||
> | ||
{item.text} | ||
</Link> | ||
); | ||
})} | ||
</div> | ||
); | ||
} | ||
|
||
import { useEffect, useState } from "react"; | ||
|
||
function useCurrentAnchor() { | ||
const [currentAnchor, setCurrentAnchor] = useState<string | null>(null); | ||
|
||
useEffect(() => { | ||
const mdxContainer: HTMLElement | null = document.querySelector( | ||
"[data-mdx-container]", | ||
); | ||
|
||
if (!mdxContainer) return; | ||
|
||
const offsetTop = 200; | ||
|
||
const observer = new IntersectionObserver( | ||
(entries) => { | ||
let currentEntry = entries[0]; | ||
if (!currentEntry) return; | ||
|
||
const offsetBottom = | ||
(currentEntry.rootBounds?.height || 0) * 0.3 + offsetTop; | ||
|
||
for (let i = 1; i < entries.length; i++) { | ||
const entry = entries[i]; | ||
if (!entry) break; | ||
|
||
if ( | ||
entry.boundingClientRect.top < | ||
currentEntry.boundingClientRect.top || | ||
currentEntry.boundingClientRect.bottom < offsetTop | ||
) { | ||
currentEntry = entry; | ||
} | ||
} | ||
|
||
let target: Element | undefined = currentEntry.target; | ||
|
||
// if the target is too high up, we need to find the next sibling | ||
while (target && target.getBoundingClientRect().bottom < offsetTop) { | ||
target = siblings.get(target)?.next; | ||
} | ||
|
||
// if the target is too low, we need to find the previous sibling | ||
while (target && target.getBoundingClientRect().top > offsetBottom) { | ||
target = siblings.get(target)?.prev; | ||
} | ||
if (target) setCurrentAnchor(target.id); | ||
}, | ||
{ | ||
threshold: 1, | ||
rootMargin: `-${offsetTop}px 0px 0px 0px`, | ||
}, | ||
); | ||
|
||
const siblings = new Map(); | ||
|
||
const anchors = mdxContainer?.querySelectorAll("[data-mdx-heading]"); | ||
anchors.forEach((anchor) => observer.observe(anchor)); | ||
|
||
return () => { | ||
observer.disconnect(); | ||
}; | ||
}, []); | ||
|
||
return currentAnchor?.replace("#", ""); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.