diff --git a/package.json b/package.json index fa741b9..b7c944a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/code-of-conduct/page.tsx b/src/app/code-of-conduct/page.tsx index eb42ebb..68a775e 100644 --- a/src/app/code-of-conduct/page.tsx +++ b/src/app/code-of-conduct/page.tsx @@ -10,7 +10,7 @@ export default function CodeOfConductPage() { return (
-

+

Code of conduct

diff --git a/src/app/cookie-policy/page.tsx b/src/app/cookie-policy/page.tsx index 00e9366..3cbb660 100644 --- a/src/app/cookie-policy/page.tsx +++ b/src/app/cookie-policy/page.tsx @@ -10,7 +10,7 @@ export default function CookiePolicyPage() { return (
-

+

Cookie policy

diff --git a/src/components/blog/BlogPost/index.tsx b/src/components/blog/BlogPost/index.tsx index 40a12c3..1f8f387 100644 --- a/src/components/blog/BlogPost/index.tsx +++ b/src/components/blog/BlogPost/index.tsx @@ -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(`/api/blog/post?slug=${slug}`); @@ -21,6 +22,8 @@ export default function BlogPostList({ slug }: { slug: string }) { return null; } + const authorLink = data.author ? getUserLink(data.author) : undefined; + return (

-
-

+

- + {/* {data.tags?.map((tag) => ( + + {tag.name} + + ))} */} - {data.coverImage ? ( ) : null}
-
-

{text}

, - list: (body: string, ordered: boolean, start?: number) => - ordered ? ( -
    - {body} -
- ) : ( -
    - {body} -
- ), - link: (href: string, text: string) => ( - {text} - ), - image: (href: string, title: string, text: string) => ( - - ), - }} +
+

- {data.content?.markdown} - + {data.title} +

+
+ {data.content?.markdown} +
+
+ +
); } diff --git a/src/components/blog/BlogPostList/BlogPostListItem.tsx b/src/components/blog/BlogPostList/BlogPostListItem.tsx index 8f503e0..15567c3 100644 --- a/src/components/blog/BlogPostList/BlogPostListItem.tsx +++ b/src/components/blog/BlogPostList/BlogPostListItem.tsx @@ -14,7 +14,12 @@ export default function BlogPostListItem({ post }: { post: HashnodePost }) { const authorLink = post.author ? getUserLink(post.author) : undefined; return ( -
+
{post.coverImage ? ( ) : (
@@ -30,15 +36,19 @@ export default function BlogPostListItem({ post }: { post: HashnodePost }) {
-
-
+ ))} */} +
-

- +

+

-

{post.brief}

+

+ {post.brief} +

-
+
-
+
); diff --git a/src/components/blog/BlogPostList/index.tsx b/src/components/blog/BlogPostList/index.tsx index 56b41cd..1c7d7b2 100644 --- a/src/components/blog/BlogPostList/index.tsx +++ b/src/components/blog/BlogPostList/index.tsx @@ -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(getKey); + const [sentryRef] = useInfiniteScroll({ + loading: isLoading || isValidating, + hasNextPage, + onLoadMore: () => setSize(size + 1), + disabled: !!error, + rootMargin: '0px 0px 400px 0px', + }); + if (!data && !error) { return null; } @@ -42,7 +55,7 @@ export default function BlogPostList() { )), )} - +
); } diff --git a/src/components/blog/MarkdownContent/Heading.tsx b/src/components/blog/MarkdownContent/Heading.tsx new file mode 100644 index 0000000..a9ef244 --- /dev/null +++ b/src/components/blog/MarkdownContent/Heading.tsx @@ -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 ( + + {children} + + ); +} diff --git a/src/components/blog/MarkdownContent/index.tsx b/src/components/blog/MarkdownContent/index.tsx new file mode 100644 index 0000000..a999859 --- /dev/null +++ b/src/components/blog/MarkdownContent/index.tsx @@ -0,0 +1,54 @@ +import Markdown from 'marked-react'; + +import Heading from '@/components/blog/MarkdownContent/Heading'; +import PrimaryLink from '@/components/common/links/PrimaryLink'; +import NextImage from '@/components/common/NextImage'; + +export default function MarkdownContent({ children }: { children?: string }) { + return ( + ( + + {text} + + ), + paragraph: (text: string) =>

{text}

, + list: (body: string, ordered: boolean, start?: number) => + ordered ? ( +
    + {body} +
+ ) : ( +
    + {body} +
+ ), + link: (href: string, text: string) => ( + {text} + ), + image: (href: string, title: string, text: string) => ( + + ), + }} + > + {children} +
+ ); +} diff --git a/yarn.lock b/yarn.lock index 1e53810..fb24816 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4401,6 +4401,18 @@ react-icons@4.11.0: resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.11.0.tgz#4b0e31c9bfc919608095cc429c4f1846f4d66c65" integrity sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA== +react-infinite-scroll-hook@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-infinite-scroll-hook/-/react-infinite-scroll-hook-4.1.1.tgz#25c0c2cb9a41039019bd723823e21db1404c137d" + integrity sha512-1bu2572rF3DtjFMhIOzoasLMdYW0vMWxROtl99M5FYGSxm84Ro4aNBZW6ivgE45ofus4Ymo7jIS0Be3zcuLk8g== + dependencies: + react-intersection-observer-hook "^2.1.1" + +react-intersection-observer-hook@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-intersection-observer-hook/-/react-intersection-observer-hook-2.1.1.tgz#6222a82624d2a507aa5ad187c99d2d530e746e4f" + integrity sha512-MeFGpYtcfHB9v6oGqQuHAbSwaWBpd7yZ4wMIeVtboWRdGusAF4V+/8QQ0OKZ36Ez19grYnoDVhRUCjtwI2ZVaw== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"