Skip to content

Commit

Permalink
feat: live updates on blog posts and blog landing page (#353)
Browse files Browse the repository at this point in the history
* feat: hydrate blog post content on the client side

* use public vars for Contentful Delivery API

* hydrate Blog landing page on the client side

* refactor: move isPressReleasePost to utils file

* hydrate all posts on rendering the blog home

* refactor: pass fetching logic to hooks

* refactor: simplify data fetching hooks

* modify useAllPosts

* add error handling

* include FIXME comment

* Revert "add error handling"

This reverts commit a6d4058.

* refactor: move util functions out of the component files

* improve FIXME description
  • Loading branch information
DiogoSoaress authored May 29, 2024
1 parent 59cd346 commit ae356c2
Show file tree
Hide file tree
Showing 15 changed files with 114 additions and 32 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ runs:
NEXT_PUBLIC_HOTJAR_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_HOTJAR_ID }}
NEXT_PUBLIC_HOTJAR_ID_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_HOTJAR_ID_STAGING }}
DUNE_API_KEY: ${{ fromJSON(inputs.secrets).DUNE_API_KEY }}
CONTENTFUL_SPACE_ID: ${{ fromJSON(inputs.secrets).CONTENTFUL_SPACE_ID }}
CONTENTFUL_ACCESS_TOKEN: ${{ fromJSON(inputs.secrets).CONTENTFUL_ACCESS_TOKEN }}
NEXT_PUBLIC_CONTENTFUL_SPACE_ID: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_CONTENTFUL_SPACE_ID }}
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN }}
NEXT_PUBLIC_PUSHWOOSH_APPLICATION_CODE: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_PUSHWOOSH_APPLICATION_CODE }}
39 changes: 22 additions & 17 deletions src/components/Blog/BlogHome/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import BlogLayout, { type MetaTagsEntry } from '@/components/Blog/Layout'
import BlogLayout from '@/components/Blog/Layout'
import { Container, Grid, Typography } from '@mui/material'
import Card from '@/components/Blog/Card'
import FeaturedPost from '@/components/Blog/FeaturedPost'
import { type BlogPostEntry } from '@/components/Blog/Post'
import SearchFilterResults from '@/components/Blog/SearchFilterResults'
import { containsTag, PRESS_RELEASE_TAG } from '@/lib/containsTag'
import type { TypeBlogHomeSkeleton, TypePostSkeleton } from '@/contentful/types'
import { type EntryCollection, type Entry } from 'contentful'
import { isEntryTypePost } from '@/lib/typeGuards'
import { useBlogHome } from '@/hooks/useBlogHome'

const categories = ['Announcements', 'Ecosystem', 'Community', 'Insights', 'Build']

const TRENDING_POSTS_COUNT = 3

export type BlogHomeEntry = Entry<TypeBlogHomeSkeleton, undefined, string>

export type BlogHomeProps = {
metaTags: MetaTagsEntry
featuredPost: BlogPostEntry
mostPopular: BlogPostEntry[]
allPosts: BlogPostEntry[]
blogHome: BlogHomeEntry
allPosts: EntryCollection<TypePostSkeleton, undefined, string>
}

const BlogHome = (props: BlogHomeProps) => {
const { featuredPost, mostPopular, allPosts, metaTags } = props
const BlogHome = ({ blogHome, allPosts }: BlogHomeProps) => {
const { data: localBlogHome } = useBlogHome(blogHome.sys.id, blogHome)

const nonPressReleases = allPosts.filter((post) => !containsTag(post.fields.tags, PRESS_RELEASE_TAG))
const { featured, metaTags, mostPopular } = localBlogHome.fields

return (
<BlogLayout metaTags={metaTags}>
Expand All @@ -37,21 +39,24 @@ const BlogHome = (props: BlogHomeProps) => {
</Grid>
</Grid>

<FeaturedPost {...featuredPost} />
{isEntryTypePost(featured) && <FeaturedPost {...featured} />}

<Typography variant="h2" mt={{ xs: '60px', md: '100px' }}>
Trending
</Typography>
<Grid container columnSpacing={2} rowGap="30px" mt="80px">
{mostPopular.slice(0, TRENDING_POSTS_COUNT).map((post) => (
<Grid key={post.fields.slug} item xs={12} md={4}>
<Card {...post} />
</Grid>
))}
{mostPopular
.filter(isEntryTypePost)
.slice(0, TRENDING_POSTS_COUNT)
.map((post) => (
<Grid key={post.fields.slug} item xs={12} md={4}>
<Card {...post} />
</Grid>
))}
</Grid>

{/* All posts */}
<SearchFilterResults allPosts={nonPressReleases} categories={categories} />
<SearchFilterResults allPosts={allPosts} categories={categories} />
</Container>
</BlogLayout>
)
Expand Down
5 changes: 4 additions & 1 deletion src/components/Blog/Post/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ import { type Document as ContentfulDocument } from '@contentful/rich-text-types
import css from '../styles.module.css'
import { PRESS_RELEASE_TAG, containsTag } from '@/lib/containsTag'
import { COMMS_EMAIL } from '@/config/constants'
import { useBlogPost } from '@/hooks/useBlogPost'

export type BlogPostEntry = Entry<TypePostSkeleton, undefined, string>

const BlogPost = ({ blogPost }: { blogPost: BlogPostEntry }) => {
const { title, excerpt, content, coverImage, authors, tags, category, date, relatedPosts, metaTags } = blogPost.fields
const { data: post } = useBlogPost(blogPost.sys.id, blogPost)

const { title, excerpt, content, coverImage, authors, tags, category, date, relatedPosts, metaTags } = post.fields

const authorsList = authors.filter(isEntryTypeAuthor)
const relatedPostsList = relatedPosts?.filter(isEntryTypePost)
Expand Down
26 changes: 22 additions & 4 deletions src/components/Blog/SearchFilterResults/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { BlogPostEntry } from '@/components/Blog/Post'
import SearchBar from '@/components/Blog/SearchBar'
import usePostsSearch from '@/components/Blog/usePostsSearch'
import { Box, Grid, Typography } from '@mui/material'
Expand All @@ -11,23 +10,42 @@ import { scrollToElement } from '@/lib/scrollSmooth'
import ShowMoreButton from '@/components/common/ShowMoreButton'
import CategoryFilter from '@/components/common/CategoryFilter'
import { getPage } from '@/lib/getPage'
import type { TypePostSkeleton } from '@/contentful/types'
import type { EntryCollection } from 'contentful'
import { useAllPosts } from '@/hooks/useAllPosts'
import { isPressReleasePost } from '@/lib/containsTag'
import { isDraft } from '@/lib/contentful/isDraft'
import { isSelectedCategory } from '@/lib/contentful/isSelectedCategory'

const PAGE_LENGTH = 6

const SearchFilterResults = ({ allPosts, categories }: { allPosts: BlogPostEntry[]; categories: string[] }) => {
const SearchFilterResults = ({
allPosts,
categories,
}: {
allPosts: EntryCollection<TypePostSkeleton, undefined, string>
categories: string[]
}) => {
const [searchQuery, setSearchQuery] = useState('')
const router = useRouter()
const selectedCategory = router.query.category
const page = getPage(router.query)

const { localAllPosts } = useAllPosts(allPosts)

const filteredPosts = useMemo(() => {
return !selectedCategory ? allPosts : allPosts.filter((post) => post.fields.category === selectedCategory)
}, [allPosts, selectedCategory])
const visiblePosts = localAllPosts.items.filter((post) => !isPressReleasePost(post) && !isDraft(post))

return typeof selectedCategory === 'string'
? visiblePosts.filter((post) => isSelectedCategory(post, selectedCategory))
: visiblePosts
}, [localAllPosts, selectedCategory])

const searchResults = usePostsSearch(filteredPosts, searchQuery)
const visibleResults = searchResults.slice(0, PAGE_LENGTH * page)
const shouldShowMoreButton = visibleResults.length < searchResults.length

// Scroll to results when navigating to the page with a category query param
useEffect(() => {
if (router.query.category) scrollToElement('#results', 250)
})
Expand Down
4 changes: 2 additions & 2 deletions src/components/Pressroom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import PressReleases from '@/components/Pressroom/PressReleases'
import Marquee from '@/components/common/Marquee'
import Timeline from '@/components/Pressroom/Timeline'
import { type TypePressRoomSkeleton } from '@/contentful/types'
import { containsTag, PRESS_RELEASE_TAG } from '@/lib/containsTag'
import { isPressReleasePost } from '@/lib/containsTag'
import {
isAsset,
isEntryType,
Expand Down Expand Up @@ -43,7 +43,7 @@ const PressRoom = ({ pressRoom, allPosts, totalAssets }: PressRoomProps) => {
const newsList = news.filter(isEntryTypeExternalURL)
const podcastsList = podcasts.filter(isEntryTypeExternalURL)
const videosList = videos.filter(isEntryTypeExternalURL)
const pressPosts = allPosts.filter((post) => containsTag(post.fields.tags, PRESS_RELEASE_TAG))
const pressPosts = allPosts.filter(isPressReleasePost)

return (
<>
Expand Down
1 change: 1 addition & 0 deletions src/contentful/types/TypePost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { TypeTagSkeleton } from './TypeTag'
export interface TypePostFields {
metaTags: EntryFieldTypes.EntryLink<TypeMetaTagsSkeleton>
title: EntryFieldTypes.Symbol
isDraft: EntryFieldTypes.Boolean
slug: EntryFieldTypes.Symbol
authors: EntryFieldTypes.Array<EntryFieldTypes.EntryLink<TypeAuthorSkeleton>>
date: EntryFieldTypes.Date
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useAllPosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type TypePostSkeleton } from '@/contentful/types'
import client from '@/lib/contentful'
import { type EntryCollection } from 'contentful'
import { useEffect, useState } from 'react'

// FIXME: This hook should make use of useSWR instead of useState but encountered issues with comparing the fallback data and fetched data
export const useAllPosts = (fallbackData: EntryCollection<TypePostSkeleton, undefined, string>) => {
const [localAllPosts, setLocalAllPosts] = useState<EntryCollection<TypePostSkeleton, undefined, string>>(fallbackData)

useEffect(() => {
client
.getEntries<TypePostSkeleton>({
content_type: 'post',
order: ['-fields.date'],
})
.then((entry) => {
setLocalAllPosts(entry)
})
}, [])

return { localAllPosts }
}
11 changes: 11 additions & 0 deletions src/hooks/useBlogHome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import useSWR from 'swr'
import client from '@/lib/contentful'
import { type BlogHomeEntry } from '@/components/Blog/BlogHome'
import { type TypeBlogHomeSkeleton } from '@/contentful/types'

const blogHomeFetcher = (id: string) => client.getEntry<TypeBlogHomeSkeleton>(id)

export const useBlogHome = (id: string, fallbackData: BlogHomeEntry) =>
useSWR(id, blogHomeFetcher, {
fallbackData,
})
8 changes: 8 additions & 0 deletions src/hooks/useBlogPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import useSWR from 'swr'
import client from '@/lib/contentful'
import { type BlogPostEntry } from '@/components/Blog/Post'
import { type TypePostSkeleton } from '@/contentful/types'

const postFetcher = (id: string) => client.getEntry<TypePostSkeleton>(id)

export const useBlogPost = (id: string, fallbackData: BlogPostEntry) => useSWR(id, postFetcher, { fallbackData })
3 changes: 3 additions & 0 deletions src/lib/containsTag.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type BlogPostEntry } from '@/components/Blog/Post'
import { type TagsType } from '@/components/Blog/Tags'
import { isEntryTypeTag } from '@/lib/typeGuards'

Expand All @@ -6,3 +7,5 @@ export const PRESS_RELEASE_TAG = 'Press'
export const containsTag = (tags: TagsType, targetTag: string) => {
return !!tags?.filter(isEntryTypeTag)?.some((item) => item.fields.name === targetTag)
}

export const isPressReleasePost = (post: BlogPostEntry) => containsTag(post.fields.tags, PRESS_RELEASE_TAG)
4 changes: 2 additions & 2 deletions src/lib/contentful.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as contentful from 'contentful'

const client = contentful.createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID,
accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN,
})

export default client
3 changes: 3 additions & 0 deletions src/lib/contentful/isDraft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { BlogPostEntry } from '@/components/Blog/Post'

export const isDraft = (post: BlogPostEntry) => post.fields.isDraft
5 changes: 5 additions & 0 deletions src/lib/contentful/isSelectedCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { TypePostSkeleton } from '@/contentful/types'
import type { Entry } from 'contentful'

export const isSelectedCategory = (post: Entry<TypePostSkeleton, undefined, string>, selectedCategory: string) =>
post.fields.category === selectedCategory
5 changes: 5 additions & 0 deletions src/lib/typeGuards.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
TypeAuthorSkeleton,
TypeBaseBlockSkeleton,
TypeBlogHomeSkeleton,
TypeButtonSkeleton,
TypeCardGridItemSkeleton,
TypeExternalUrlSkeleton,
Expand Down Expand Up @@ -52,6 +53,10 @@ export const isEntryTypeBaseBlock = (obj: any): obj is Entry<TypeBaseBlockSkelet
return getContentTypeSysId(obj) === 'baseBlock'
}

export const isEntryTypeBlogHome = (obj: any): obj is Entry<TypeBlogHomeSkeleton, undefined, string> => {
return getContentTypeSysId(obj) === 'blogHome'
}

export const isEntryType = (obj: any): obj is Entry => {
return obj.sys.type === 'Entry'
}
Expand Down
6 changes: 2 additions & 4 deletions src/pages/blog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@ export const getStaticProps = async () => {

return {
props: {
metaTags: blogHome.fields.metaTags,
featuredPost: blogHome.fields.featured,
mostPopular: blogHome.fields.mostPopular,
allPosts: postsEntries.items,
blogHome,
allPosts: postsEntries,
},
}
}
Expand Down

0 comments on commit ae356c2

Please sign in to comment.