Skip to content

Commit

Permalink
feat: infinite scroll blog list & markdown heading styles
Browse files Browse the repository at this point in the history
  • Loading branch information
TheCatLady committed Oct 12, 2023
1 parent 3212250 commit 9bba99a
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 81 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"react-google-recaptcha": "3.1.0",
"react-hook-form": "7.47.0",
"react-icons": "4.11.0",
"react-infinite-scroll-hook": "4.1.1",
"swr": "2.2.4",
"tailwind-merge": "1.14.0",
"zod": "3.22.4",
Expand Down
2 changes: 1 addition & 1 deletion src/app/code-of-conduct/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function CodeOfConductPage() {
return (
<div className="px-6 py-32 lg:px-8">
<div className="mx-auto max-w-3xl text-base leading-7 text-gray-700">
<h1 className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
<h1 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Code of conduct
</h1>
<div className="max-w-2xl">
Expand Down
2 changes: 1 addition & 1 deletion src/app/cookie-policy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function CookiePolicyPage() {
return (
<div className="px-6 py-32 lg:px-8">
<div className="mx-auto max-w-3xl text-base leading-7 text-gray-700">
<h1 className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
<h1 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Cookie policy
</h1>
<p className="mt-8 text-xl leading-8">
Expand Down
111 changes: 61 additions & 50 deletions src/components/blog/BlogPost/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
'use client';

import Markdown from 'marked-react';
import useSWR from 'swr';

import PrimaryLink from '@/components/common/links/PrimaryLink';
import MarkdownContent from '@/components/blog/MarkdownContent';
import UnstyledLink from '@/components/common/links/UnstyledLink';
import NextImage from '@/components/common/NextImage';

import { siteConfig } from '@/constants/config';
import { HashnodePost } from '@/interfaces/hashnode';
import { getUserLink } from '@/utils/hashnode';

export default function BlogPostList({ slug }: { slug: string }) {
export default function BlogPost({ slug }: { slug: string }) {
const url = `${siteConfig.url}/blog/${slug}`;
const { data, error } = useSWR<HashnodePost>(`/api/blog/post?slug=${slug}`);

Expand All @@ -21,75 +22,85 @@ export default function BlogPostList({ slug }: { slug: string }) {
return null;
}

const authorLink = data.author ? getUserLink(data.author) : undefined;

return (
<article
className="mx-auto max-w-3xl text-lg leading-7 text-gray-700"
itemProp="blogPost"
itemScope
itemType="http://schema.org/BlogPosting"
>
<header>
<h1
className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl"
itemProp="headline"
<header className="flex items-center gap-x-4 text-base">
<time
dateTime={data.publishedAt}
className="text-gray-500"
itemProp="datePublished"
>
{data.title}
</h1>
<time dateTime={data.publishedAt} itemProp="datePublished">
{new Date(data.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
{/* {data.tags?.map((tag) => (
<UnstyledLink
href={`/blog/tags/${tag.slug}`}
className="relative z-10 rounded-full bg-gray-50 px-3 py-1.5 font-medium text-gray-600 hover:bg-gray-100"
key={`tag-${tag.slug}`}
>
{tag.name}
</UnstyledLink>
))} */}
<meta itemProp="description" content={data.brief} />
<meta itemProp="url" content={url} />
<meta itemProp="identifier" content={url} />
{data.coverImage ? (
<meta itemProp="image" content={data.coverImage.url} />
) : null}
</header>
<div className="w-prose" itemProp="articleBody">
<Markdown
renderer={{
paragraph: (text: string) => <p className="mt-8">{text}</p>,
list: (body: string, ordered: boolean, start?: number) =>
ordered ? (
<ol
role="list"
className="mt-8 max-w-xl space-y-8 text-gray-600"
start={start}
>
{body}
</ol>
) : (
<ul
role="list"
className="mt-8 max-w-xl space-y-8 text-gray-600"
>
{body}
</ul>
),
link: (href: string, text: string) => (
<PrimaryLink href={href}>{text}</PrimaryLink>
),
image: (href: string, title: string, text: string) => (
<NextImage
src={href}
alt={title}
title={text}
className="h-max w-full"
classNames={{ image: 'h-auto w-auto object-none mx-auto' }}
width={0}
height={0}
sizes="100vw"
/>
),
}}
<div>
<h1
className="mt-3 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl"
itemProp="headline"
>
{data.content?.markdown}
</Markdown>
{data.title}
</h1>
</div>
<div className="w-prose" itemProp="articleBody">
<MarkdownContent>{data.content?.markdown}</MarkdownContent>
</div>
<footer className="mt-6 flex border-t border-gray-900/5 pt-6">
<div
className="relative flex items-center gap-x-4"
itemProp="author"
itemScope
itemType="https://schema.org/Person"
>
<NextImage
src={data.author.profilePicture}
alt=""
className="h-10 w-10 shrink-0 overflow-hidden rounded-full bg-gray-50"
classNames={{ image: 'w-full h-full object-cover' }}
width={40}
height={40}
/>
<div className="text-base leading-6">
<p className="font-semibold text-gray-900" itemProp="name">
{authorLink ? (
<UnstyledLink href={authorLink} itemProp="url">
<span className="absolute inset-0" />
{data.author.name}
</UnstyledLink>
) : (
<>{data.author.name}</>
)}
</p>
<p className="line-clamp-1 text-gray-600" itemProp="description">
{data.author.tagline}
</p>
</div>
</div>
</footer>
</article>
);
}
40 changes: 29 additions & 11 deletions src/components/blog/BlogPostList/BlogPostListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export default function BlogPostListItem({ post }: { post: HashnodePost }) {
const authorLink = post.author ? getUserLink(post.author) : undefined;

return (
<article className="relative isolate flex flex-col gap-8 lg:flex-row">
<article
className="relative isolate flex flex-col gap-8 lg:flex-row"
itemProp="blogPost"
itemScope
itemType="http://schema.org/BlogPosting"
>
<div className="relative aspect-[16/9] sm:aspect-[2/1] lg:aspect-square lg:w-64 lg:shrink-0">
{post.coverImage ? (
<NextImage
Expand All @@ -23,41 +28,54 @@ export default function BlogPostListItem({ post }: { post: HashnodePost }) {
className="absolute inset-0 h-full w-full overflow-hidden rounded-2xl bg-gray-50"
classNames={{ image: 'object-cover' }}
layout="fill"
itemProp="image"
/>
) : (
<div className="absolute inset-0 h-full w-full rounded-2xl bg-gray-50 object-cover" />
)}
<div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10" />
</div>
<div>
<div className="flex items-center gap-x-4 text-sm">
<time dateTime={post.publishedAt} className="text-gray-500">
<header className="flex items-center gap-x-4 text-sm">
<time
dateTime={post.publishedAt}
className="text-gray-500"
itemProp="datePublished"
>
{new Date(post.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
{post.tags?.map((tag) => (
{/* {post.tags?.map((tag) => (
<UnstyledLink
href={`/blog/tags/${tag.slug}`}
className="relative z-10 rounded-full bg-gray-50 px-3 py-1.5 font-medium text-gray-600 hover:bg-gray-100"
key={`tag-${tag.slug}`}
>
{tag.name}
</UnstyledLink>
))}
</div>
))} */}
</header>
<div className="group relative max-w-xl">
<h3 className="mt-3 text-xl font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
<UnstyledLink href={`/blog/${post.slug}`}>
<h3
className="mt-3 text-xl font-semibold leading-6 text-gray-900 group-hover:text-gray-600"
itemProp="headline"
>
<UnstyledLink href={`/blog/${post.slug}`} itemProp="url">
<span className="absolute inset-0" />
{post.title}
</UnstyledLink>
</h3>
<p className="mt-5 text-base leading-6 text-gray-600">{post.brief}</p>
<p
className="mt-5 text-base leading-6 text-gray-600"
itemProp="description"
>
{post.brief}
</p>
</div>
<div className="mt-6 flex border-t border-gray-900/5 pt-6">
<footer className="mt-6 flex border-t border-gray-900/5 pt-6">
<div className="relative flex items-center gap-x-4">
<NextImage
src={post.author.profilePicture}
Expand All @@ -83,7 +101,7 @@ export default function BlogPostListItem({ post }: { post: HashnodePost }) {
</p>
</div>
</div>
</div>
</footer>
</div>
</article>
);
Expand Down
49 changes: 31 additions & 18 deletions src/components/blog/BlogPostList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
'use client';

import { useState } from 'react';
import useInfiniteScroll from 'react-infinite-scroll-hook';
import useSWRInfinite from 'swr/infinite';

import BlogPostListItem from '@/components/blog/BlogPostList/BlogPostListItem';

import { HashnodePostEdge } from '@/interfaces/hashnode';

const getKey = (pageIndex: number, previousPageData: HashnodePostEdge[]) => {
const endpoint = '/api/blog/posts';
export default function BlogPostList() {
const [hasNextPage, setHasNextPage] = useState(true);

// reached the end
if (previousPageData && previousPageData.length === 0) {
return null;
}
const getKey = (pageIndex: number, previousPageData: HashnodePostEdge[]) => {
const endpoint = '/api/blog/posts';

// first page, we don't have `previousPageData`
if (pageIndex === 0) {
return endpoint;
}
// reached the end
if (previousPageData && previousPageData.length === 0) {
setHasNextPage(false);
return null;
}

// add the cursor to the API endpoint
return `${endpoint}?after=${
previousPageData[previousPageData.length - 1].cursor
}`;
};
// first page, we don't have `previousPageData`
if (pageIndex === 0) {
return endpoint;
}

export default function BlogPostList() {
const { data, error, size, setSize } =
// add the cursor to the API endpoint
return `${endpoint}?after=${
previousPageData[previousPageData.length - 1].cursor
}`;
};

const { data, error, size, setSize, isLoading, isValidating } =
useSWRInfinite<HashnodePostEdge[]>(getKey);

const [sentryRef] = useInfiniteScroll({
loading: isLoading || isValidating,
hasNextPage,
onLoadMore: () => setSize(size + 1),
disabled: !!error,
rootMargin: '0px 0px 400px 0px',
});

if (!data && !error) {
return null;
}
Expand All @@ -42,7 +55,7 @@ export default function BlogPostList() {
<BlogPostListItem post={post} key={`post-${post.slug}`} />
)),
)}
<button onClick={() => setSize(size + 1)}>Load More</button>
<div ref={sentryRef} />
</div>
);
}
32 changes: 32 additions & 0 deletions src/components/blog/MarkdownContent/Heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ReactNode } from 'react';

import { cn } from '@/utils/css';

const headingClasses = {
h2: 'mt-16 mb-8 text-2xl',
h3: 'mt-12 mb-8 text-xl',
h4: 'my-8 text-lg',
h5: 'my-8 text-base',
h6: 'my-8 text-base',
};

export default function Heading({
as: As,
children,
...props
}: {
as: 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
children: ReactNode;
}) {
return (
<As
{...props}
className={cn(
'font-bold tracking-tight text-gray-900',
headingClasses[As],
)}
>
{children}
</As>
);
}
Loading

0 comments on commit 9bba99a

Please sign in to comment.