mirror of
https://github.com/r-freeman/portfolio.git
synced 2024-11-14 19:55:40 +00:00
Added Article navigation
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 2m45s
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 2m45s
This commit is contained in:
parent
9bd9188921
commit
5e8da79a82
7
app/api/articles/route.ts
Normal file
7
app/api/articles/route.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {getAllArticles} from '@/lib/getAllArticles'
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const articles = await getAllArticles(false)
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(articles), {status: 200})
|
||||||
|
}
|
@ -5,6 +5,7 @@ import {Prose} from '@/components/ui/Prose'
|
|||||||
import {Views} from '@/components/ui/Views'
|
import {Views} from '@/components/ui/Views'
|
||||||
import {ArrowDownIcon} from '@/components/icons/ArrowDownIcon'
|
import {ArrowDownIcon} from '@/components/icons/ArrowDownIcon'
|
||||||
import {formatDate} from '@/lib/formatDate'
|
import {formatDate} from '@/lib/formatDate'
|
||||||
|
import ArticleNav from '@/components/ui/ArticleNav'
|
||||||
|
|
||||||
type ArticleLayout = {
|
type ArticleLayout = {
|
||||||
title: string
|
title: string
|
||||||
@ -12,7 +13,6 @@ type ArticleLayout = {
|
|||||||
description: string
|
description: string
|
||||||
slug: string
|
slug: string
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
ogImage?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const gradients = [
|
const gradients = [
|
||||||
@ -26,10 +26,8 @@ const gradients = [
|
|||||||
export function ArticleLayout({
|
export function ArticleLayout({
|
||||||
title,
|
title,
|
||||||
date,
|
date,
|
||||||
description,
|
|
||||||
slug,
|
slug,
|
||||||
children,
|
children,
|
||||||
ogImage
|
|
||||||
}: ArticleLayout) {
|
}: ArticleLayout) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -60,6 +58,7 @@ export function ArticleLayout({
|
|||||||
</header>
|
</header>
|
||||||
<Prose className="mt-8" data-mdx-content>{children}</Prose>
|
<Prose className="mt-8" data-mdx-content>{children}</Prose>
|
||||||
</article>
|
</article>
|
||||||
|
<ArticleNav slug={slug}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
89
components/ui/ArticleNav.tsx
Normal file
89
components/ui/ArticleNav.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, {ReactElement, useEffect, useState} from 'react'
|
||||||
|
import fetcher from '@/lib/fetcher'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import {Card} from '@/components/ui/Card'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
|
||||||
|
type Article = {
|
||||||
|
slug: string
|
||||||
|
authors: string
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchArticlesResponse = {
|
||||||
|
data: Article[]
|
||||||
|
error: string
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleNavProps = {
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationProps = {
|
||||||
|
next: Article | null
|
||||||
|
prev: Article | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFetchArticles() {
|
||||||
|
const {data, error, isLoading} = useSWR(`/api/articles/`, fetcher) as FetchArticlesResponse
|
||||||
|
|
||||||
|
return {
|
||||||
|
articles: data,
|
||||||
|
isLoading,
|
||||||
|
isError: error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArticleNav({slug}: ArticleNavProps): ReactElement | null {
|
||||||
|
const {articles, isLoading, isError} = useFetchArticles()
|
||||||
|
const [{next, prev}, setPagination] = useState<PaginationProps>({next: null, prev: null})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const findAdjacentArticles = (articles: Article[], slug: string) => {
|
||||||
|
if (articles) {
|
||||||
|
const index = articles.findIndex(article => article.slug === slug)
|
||||||
|
const next = index < articles.length - 1 ? articles[index + 1] : null
|
||||||
|
const prev = index > 0 ? articles[index - 1] : null
|
||||||
|
|
||||||
|
setPagination({next, prev})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findAdjacentArticles(articles, slug)
|
||||||
|
}, [articles, slug])
|
||||||
|
|
||||||
|
if (isError) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mt-24">
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className={clsx('grid grid-cols-1 gap-x-12 gap-y-16',
|
||||||
|
(prev !== null && next !== null) ? 'sm:grid-cols-2' : '')}
|
||||||
|
>
|
||||||
|
{prev !== null &&
|
||||||
|
<Card as="li" key={prev.slug}>
|
||||||
|
<h2 className="text-base font-semibold transition group-hover:text-indigo-500 text-zinc-800 dark:text-zinc-100">
|
||||||
|
<Card.Link href={`/writing/${prev.slug}`}
|
||||||
|
ariaLabel={`Previous article: ${prev.title}`}>{prev.title}</Card.Link>
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
{next !== null &&
|
||||||
|
<Card as="li" key={next.slug}>
|
||||||
|
<h2 className="text-base font-semibold transition group-hover:text-indigo-500 text-zinc-800 dark:text-zinc-100">
|
||||||
|
<Card.Link href={`/writing/${next.slug}`}
|
||||||
|
ariaLabel={`Next article: ${next.title}`}>{next.title}</Card.Link>
|
||||||
|
</h2>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
@ -13,6 +13,7 @@ type Card = {
|
|||||||
|
|
||||||
type CardLink = {
|
type CardLink = {
|
||||||
href: string
|
href: string
|
||||||
|
ariaLabel?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,12 +75,12 @@ export function Card({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Card.Link = function CardLink({href, children}: CardLink) {
|
Card.Link = function CardLink({href, children, ariaLabel}: CardLink) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl"/>
|
className="absolute -inset-y-6 -inset-x-4 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl"/>
|
||||||
<Link href={href}>
|
<Link href={href} aria-label={ariaLabel}>
|
||||||
<span className="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl"/>
|
<span className="absolute -inset-y-6 -inset-x-4 z-20 sm:-inset-x-6 sm:rounded-2xl"/>
|
||||||
<span className="relative z-10">{children}</span>
|
<span className="relative z-10">{children}</span>
|
||||||
</Link>
|
</Link>
|
||||||
@ -89,7 +90,8 @@ Card.Link = function CardLink({href, children}: CardLink) {
|
|||||||
|
|
||||||
Card.Title = function CardTitle({as: Component = 'h2', href, children}: CardTitle) {
|
Card.Title = function CardTitle({as: Component = 'h2', href, children}: CardTitle) {
|
||||||
return (
|
return (
|
||||||
<Component className="group-hover:text-indigo-500 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
<Component
|
||||||
|
className="group-hover:text-indigo-500 text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||||
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
|
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
|
||||||
</Component>
|
</Component>
|
||||||
)
|
)
|
||||||
|
@ -12,12 +12,13 @@ async function importArticle(articleFilename: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllArticles() {
|
export async function getAllArticles(dateDesc = true) {
|
||||||
let articleFilenames = await glob(['*.mdx', '*/page.mdx'], {
|
let articleFilenames = await glob(['*.mdx', '*/page.mdx'], {
|
||||||
cwd: path.join(process.cwd(), './app/writing'),
|
cwd: path.join(process.cwd(), './app/writing'),
|
||||||
})
|
})
|
||||||
|
|
||||||
let articles = await Promise.all(articleFilenames.map(importArticle))
|
let articles = await Promise.all(articleFilenames.map(importArticle))
|
||||||
|
|
||||||
return articles.sort((a, z) => a.date < z.date ? 1 : -1)
|
return dateDesc ? articles.sort((a, z) => a.date < z.date ? 1 : -1)
|
||||||
|
: articles.sort((a, z) => a.date > z.date ? 1 : -1)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user