232 lines
7.2 KiB
TypeScript
232 lines
7.2 KiB
TypeScript
'use client'
|
|
|
|
import { Button } from '@/components/button'
|
|
import { Container } from '@/components/container'
|
|
import { Footer } from '@/components/footer'
|
|
import { GradientBackground } from '@/components/gradient'
|
|
import { Link } from '@/components/link'
|
|
import { Navbar } from '@/components/navbar'
|
|
import { fetchPostAuthors } from '@/components/post'
|
|
import { Heading, Lead, Subheading } from '@/components/text'
|
|
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/16/solid'
|
|
import { clsx } from 'clsx'
|
|
import { allPosts } from 'content-collections'
|
|
import dayjs from 'dayjs'
|
|
import { notFound, useSearchParams } from 'next/navigation'
|
|
|
|
const postsPerPage = 5
|
|
|
|
function FeaturedPosts() {
|
|
const featuredPosts = allPosts.slice(0, 3)
|
|
|
|
if (featuredPosts.length === 0) {
|
|
return
|
|
}
|
|
|
|
const postAuthors = fetchPostAuthors()
|
|
|
|
return (
|
|
<div className="mt-16 bg-linear-to-t from-gray-100 pb-14">
|
|
<Container>
|
|
<h2 className="text-2xl font-medium tracking-tight">Featured</h2>
|
|
<div className="mt-6 grid grid-cols-1 gap-8 lg:grid-cols-3">
|
|
{featuredPosts.map((post) => (
|
|
<div
|
|
key={post._meta.path}
|
|
className="relative flex flex-col rounded-3xl bg-white p-2 shadow-md ring-1 shadow-black/5 ring-black/5"
|
|
>
|
|
{post.image && (
|
|
<img
|
|
alt={post.title}
|
|
src={post.image}
|
|
className="aspect-3/2 w-full rounded-2xl object-cover"
|
|
/>
|
|
)}
|
|
<div className="flex flex-1 flex-col p-8">
|
|
<div className="text-sm/5 text-gray-700">
|
|
{dayjs(post.date).format('dddd, MMMM D, YYYY')}
|
|
</div>
|
|
<div className="mt-2 text-base/7 font-medium">
|
|
<Link href={post.url}>
|
|
<span className="absolute inset-0" />
|
|
{post.title}
|
|
</Link>
|
|
</div>
|
|
<div className="mt-2 flex-1 text-sm/6 text-gray-500">
|
|
{post.excerpt}
|
|
</div>
|
|
{postAuthors[post.author ?? ''] && (
|
|
<div className="mt-6 flex items-center gap-3">
|
|
<img
|
|
alt=""
|
|
src={postAuthors[post.author ?? ''].avatar}
|
|
className="aspect-square size-6 rounded-full object-cover"
|
|
/>
|
|
<div className="text-sm/5 text-gray-700">
|
|
{postAuthors[post.author ?? ''].name}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Container>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Posts({ page, category }: { page: number; category?: string }) {
|
|
let posts = allPosts.slice((page - 1) * postsPerPage, page * postsPerPage)
|
|
|
|
if (posts.length === 0 && (page > 1 || category)) {
|
|
notFound()
|
|
}
|
|
|
|
if (posts.length === 0) {
|
|
return <p className="mt-6 text-gray-500">No posts found.</p>
|
|
}
|
|
|
|
const postAuthors = fetchPostAuthors()
|
|
|
|
return (
|
|
<div className="mt-6">
|
|
{posts.map((post) => (
|
|
<div
|
|
key={post._meta.path}
|
|
className="relative grid grid-cols-1 border-b border-b-gray-100 py-10 first:border-t first:border-t-gray-200 max-sm:gap-3 sm:grid-cols-3"
|
|
>
|
|
<div>
|
|
<div className="text-sm/5 max-sm:text-gray-700 sm:font-medium">
|
|
{dayjs(post.date).format('dddd, MMMM D, YYYY')}
|
|
</div>
|
|
{postAuthors[post.author ?? ''] && (
|
|
<div className="mt-2.5 flex items-center gap-3">
|
|
{
|
|
<img
|
|
alt=""
|
|
src={postAuthors[post.author ?? ''].avatar}
|
|
className="aspect-square size-6 rounded-full object-cover"
|
|
/>
|
|
}
|
|
<div className="text-sm/5 text-gray-700">
|
|
{postAuthors[post.author ?? ''].name}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="sm:col-span-2 sm:max-w-2xl">
|
|
<h2 className="text-sm/5 font-medium">{post.title}</h2>
|
|
<p className="mt-3 text-sm/6 text-gray-500">{post.excerpt}</p>
|
|
<div className="mt-4">
|
|
<Link
|
|
href={post.url}
|
|
className="flex items-center gap-1 text-sm/5 font-medium"
|
|
>
|
|
<span className="absolute inset-0" />
|
|
Read more
|
|
<ChevronRightIcon className="size-4 fill-gray-400" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Pagination({
|
|
page,
|
|
category,
|
|
}: {
|
|
page: number
|
|
category?: string
|
|
}) {
|
|
function url(page: number) {
|
|
let params = new URLSearchParams()
|
|
|
|
if (category) params.set('category', category)
|
|
if (page > 1) params.set('page', page.toString())
|
|
|
|
return params.size !== 0 ? `/news?${params.toString()}` : '/news'
|
|
}
|
|
|
|
let totalPosts = allPosts.length
|
|
let hasPreviousPage = page - 1
|
|
let previousPageUrl = hasPreviousPage ? url(page - 1) : undefined
|
|
let hasNextPage = page * postsPerPage < totalPosts
|
|
let nextPageUrl = hasNextPage ? url(page + 1) : undefined
|
|
let pageCount = Math.ceil(totalPosts / postsPerPage)
|
|
|
|
if (pageCount < 2) {
|
|
return
|
|
}
|
|
|
|
return (
|
|
<div className="mt-6 flex items-center justify-between gap-2">
|
|
<Button
|
|
variant="outline"
|
|
href={previousPageUrl}
|
|
disabled={!previousPageUrl}
|
|
>
|
|
<ChevronLeftIcon className="size-4" />
|
|
Previous
|
|
</Button>
|
|
<div className="flex gap-2 max-sm:hidden">
|
|
{Array.from({ length: pageCount }, (_, i) => (
|
|
<Link
|
|
key={i + 1}
|
|
href={url(i + 1)}
|
|
data-active={i + 1 === page ? true : undefined}
|
|
className={clsx(
|
|
'size-7 rounded-lg text-center text-sm/7 font-medium',
|
|
'data-hover:bg-gray-100',
|
|
'data-active:shadow-sm data-active:ring-1 data-active:ring-black/10',
|
|
'data-active:data-hover:bg-gray-50',
|
|
)}
|
|
>
|
|
{i + 1}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
<Button variant="outline" href={nextPageUrl} disabled={!nextPageUrl}>
|
|
Next
|
|
<ChevronRightIcon className="size-4" />
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function News() {
|
|
const paramsPage = useSearchParams().get('page');
|
|
let page =
|
|
paramsPage
|
|
? typeof paramsPage === 'string' && parseInt(paramsPage) > 1
|
|
? parseInt(paramsPage)
|
|
: notFound()
|
|
: 1
|
|
|
|
return (
|
|
<main className="overflow-hidden">
|
|
<GradientBackground />
|
|
<Container>
|
|
<Navbar />
|
|
<Subheading className="mt-16">News</Subheading>
|
|
<Heading as="h1" className="mt-2">
|
|
What's about to drop?
|
|
</Heading>
|
|
<Lead className="mt-6 max-w-3xl">
|
|
Check out what's new and what's upcoming in Drop through our
|
|
hand-written articles by the core team of Drop.
|
|
</Lead>
|
|
</Container>
|
|
{page === 1 && <FeaturedPosts />}
|
|
<Container className="pb-24">
|
|
<Posts page={page} />
|
|
<Pagination page={page} />
|
|
</Container>
|
|
<Footer />
|
|
</main>
|
|
)
|
|
}
|