Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dotenv": "^8.2.0",
"escape-html": "^1.0.3",
"express": "^4.19.2",
"isomorphic-dompurify": "^2.15.0",
"next": "13.5.2",
"payload": "^2.0.0",
"payload-admin-bar": "^1.0.6",
Expand Down
2 changes: 1 addition & 1 deletion src/app/(pages)/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'

import { Page } from '../../../payload/payload-types'
import { staticHome } from './staticHome'
import { fetchDoc } from '../../_api/fetchDoc'
import { fetchDocs } from '../../_api/fetchDocs'
import { Blocks } from '../../_components/Blocks'
import { Hero } from '../../_components/Hero'
import { generateMeta } from '../../_utilities/generateMeta'
import { staticHome } from './staticHome'

// Payload Cloud caches all files through Cloudflare, so we don't need Next.js to cache them as well
// This means that we can turn off Next.js data caching and instead rely solely on the Cloudflare CDN
Expand Down
2 changes: 1 addition & 1 deletion src/app/(pages)/authors/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import BackButton from '../../../_components/BackButton'

export default async function ContributorPage({ params: { slug } }) {
// TODO: update fetchDoc to include error handling instead of making it on-page
const author = await fetchDoc({collection: 'authors', slug})
const author = await fetchDoc({ collection: 'authors', slug })
const contentFromAuthor = await fetchContentFromAuthor({ authorID: author.id })

if (!author) {
Expand Down
79 changes: 65 additions & 14 deletions src/app/(pages)/blogposts/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,79 @@
import React from 'react'
import { notFound } from 'next/navigation'

import { fetchDoc } from "../../../_api/fetchDoc"
import { Blogpost } from '../../../../payload/payload-types'
import { fetchDoc } from '../../../_api/fetchDoc'
import BlogpostContent from '../../../_blocks/Blogpost'
import { RecommendedContent } from '../../../_blocks/RecommendedContent'
import { Subscribe } from '../../../_blocks/Subscribe'
import BackButton from '../../../_components/BackButton'
import ContentCard from '../../../_components/ContentCard'
import PostSummary from '../../../_components/PostSummary'

export default async function BlogpostPage({ params: { slug } }) {
let episode = null
const blogpost: Blogpost | null = await fetchDoc({
collection: 'blogposts',
slug,
})

try {
episode = await fetchDoc({
collection: 'blogposts',
slug,
})
} catch (err) {
console.error(err)
}
// TODO: implement a fetcher for related content to populate related cards

if (!episode) {
if (!blogpost) {
notFound()
}

const { relatedPosts } = blogpost
return (
<div>
hello, world!
<pre>{JSON.stringify(episode, null, 2)}</pre>
<div style={{ background: 'purple' }}>
{/* Head Block*/}
<BackButton />
<PostSummary post={blogpost} />
</div>
<div style={{ display: 'flex' }}>
{/* Left column: Navigation */}
<div
style={{
background: 'white',
color: 'black',
flex: '1',
padding: '10px',
borderRight: '1px solid black',
}}
>
<h1>Table of contents block</h1>
</div>

{/* Middle column: Content block */}
<div style={{ background: 'white', color: 'black', flex: '2', padding: '10px' }}>
<BlogpostContent post={blogpost} />
</div>

{/* Right column: Social sharing & recommended */}
<div
style={{
background: 'white',
color: 'black',
flex: '1',
padding: '10px',
borderLeft: '1px solid black',
}}
>
<div>
<h1>Share block goes here</h1>
<p>SocialMedia block with links</p>
</div>
<div>
<h1>Category block</h1>
</div>
<div>
<h1>Recommended Block</h1>
<ContentCard contentType={'Blogpost'} content={blogpost} />
<ContentCard contentType={'Blogpost'} content={blogpost} />
</div>
</div>
</div>
<RecommendedContent relatedContent={relatedPosts} />
<Subscribe />
</div>
)
}
2 changes: 1 addition & 1 deletion src/app/(pages)/case-studies/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { notFound } from 'next/navigation'

import { fetchDoc } from "../../../_api/fetchDoc"
import { fetchDoc } from '../../../_api/fetchDoc'

export default async function CaseStudiesPage({ params: { slug } }) {
let content = null
Expand Down
2 changes: 1 addition & 1 deletion src/app/(pages)/podcast-episodes/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { notFound } from 'next/navigation'

import { fetchDoc } from '../../../_api/fetchDoc'
import EpisodeContent from '../../../_blocks/EpisodeContent'
import EpisodeHead from '../../../_blocks/EpisodeHead'
import EpisodeHead from '../../../_blocks/EpisodeHead'
import { RecommendedContent } from '../../../_blocks/RecommendedContent'
import { Subscribe } from '../../../_blocks/Subscribe'

Expand Down
3 changes: 2 additions & 1 deletion src/app/(pages)/talks-and-roundtables/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { notFound } from 'next/navigation'
import { fetchDoc } from "../../../_api/fetchDoc"

import { fetchDoc } from '../../../_api/fetchDoc'

export default async function TalksAndRoundTablesPage({ params: { slug } }) {
let content = null
Expand Down
3 changes: 1 addition & 2 deletions src/app/_blocks/AuthorContentGrid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ export default function AuthorContentGrid({ content }: AuthorContentGridProps) {
>
<ContentCard contentType={key} content={contentPiece} />
</div>
)),
)}
)),)}
</div>
</div>
</div>
Expand Down
17 changes: 17 additions & 0 deletions src/app/_blocks/Blogpost/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import DOMPurify from 'isomorphic-dompurify'

import { Blogpost } from '../../../payload/payload-types'
import EpisodeFeaturedImage from '../../_components/EpisodeFeaturedImage'

export default function BlogpostContent({ post }: { post: Blogpost }) {
const { summary, content_html, featuredImage } = post
const sanitizedContent = DOMPurify.sanitize(content_html)

return (
<div>
<EpisodeFeaturedImage src={featuredImage} />
<div>{summary}</div>
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
</div>
)
}
2 changes: 1 addition & 1 deletion src/app/_blocks/EpisodeHead/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import ContentTypePill from '../../_components/ContentTypePill'
import EpisodeFeaturedImage from '../../_components/EpisodeFeaturedImage'
import { formatDateTime } from '../../_utilities/formatDateTime'
import { getAudio } from '../../_utilities/getAudio'
import { useEpisodeDuration } from '../../_utilities/useEpisodeDuration'
import { getImage } from '../../_utilities/getImage'
import { useEpisodeDuration } from '../../_utilities/useEpisodeDuration'

export default function EpisodeHead({ episode }) {
const { title, episodeFile, publishedAt, featuredImage } = episode
Expand Down
11 changes: 5 additions & 6 deletions src/app/_components/AuthorPill/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { getImage } from "@/app/_utilities/getImage";
import { getImage } from '@/app/_utilities/getImage'

export default function AuthorPill({author}) {
export default function AuthorPill({ author }) {
return (
<div style={{outlineStyle: 'solid', outlineColor: 'blue', width: 144}}>
<div style={{display: 'flex'}}>
<img style={{width: 32, height: 32}} src={getImage(author.featuredImage)}/>
<div style={{ outlineStyle: 'solid', outlineColor: 'blue', width: 144 }}>
<div style={{ display: 'flex' }}>
<img style={{ width: 32, height: 32 }} src={getImage(author.featuredImage)} />
<span>{author.name}</span>
</div>

</div>
)
}
2 changes: 1 addition & 1 deletion src/app/_components/AuthorSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'

import { Author } from '../../../payload/payload-types'
import FeaturedImage from '../FeaturedImage'
import SocialLinks from '../SocialLinks'
import { Author } from '../../../payload/payload-types'

export default function AuthorSummary({ author }: { author: Author }) {
const { name, role, bio, linkedIn, gitHub, medium, x, featuredImage } = author
Expand Down
7 changes: 4 additions & 3 deletions src/app/_components/Authors/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import AuthorPill from "@/app/_components/AuthorPill";
import React from "react";
import React from 'react'

export default function Authors({authors}) {
import AuthorPill from '@/app/_components/AuthorPill'

export default function Authors({ authors }) {
return (
<div>
<div style={{ display: 'flex' }}>
Expand Down
14 changes: 14 additions & 0 deletions src/app/_components/BlogPostArchiveButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function ArchiveButton({ collection }: { collection: string }) {
function formatArchiveButton(text: string): string {
// TODO: Extend to format talks-and-roundtables to Talks & Roundtables
// TODO: Extend to fromat case-studies to Case Studies

return text.charAt(0).toLowerCase() + text.slice(1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for, to turn the word into lower case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a converter to convert what ArchiveButton receives and make a compatible key of collections

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as just text.toLowerCase() though

}

return (
<div>
<a href={`/${collection}`}>{formatArchiveButton(collection)}</a>
</div>
)
}
12 changes: 7 additions & 5 deletions src/app/_components/Container/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from "react";
import React from 'react'

export default function Container({ totalArticles, children }) {
return <div style={{ background: "white", color: "black" }}>
<div style={{ textAlign: "right" }}>{totalArticles} Articles</div>
{children}
</div>
return (
<div style={{ background: 'white', color: 'black' }}>
<div style={{ textAlign: 'right' }}>{totalArticles} Articles</div>
{children}
</div>
)
}
2 changes: 1 addition & 1 deletion src/app/_components/ContentCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { formatDateTime } from '../../_utilities/formatDateTime'
import { getImage } from '../../_utilities/getImage'
import { toKebabCase } from '../../_utilities/toKebabCase'
import Authors from '../Authors'
import EpisodeFeaturedImage from '../EpisodeFeaturedImage'
import CategoryPill from '../CategoryPill'
import EpisodeFeaturedImage from '../EpisodeFeaturedImage'

interface ContentSummaryProps {
contentType: string
Expand Down
8 changes: 8 additions & 0 deletions src/app/_components/ContentRenderer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import DOMpurify from 'isomorphic-dompurify'

export default function ContentRenderer({ content_html }: { content_html: string }) {
// Sanitize HTML content to prevent XSS vulnerabilities.
const sanitizedContent = DOMpurify.sanitize(content_html)

return <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
}
1 change: 0 additions & 1 deletion src/app/_components/FeaturedImage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getImage } from '../../_utilities/getImage'

export default function FeaturedImage({ src }) {

return (
<div style={{ width: 120, height: 120, marginRight: '20px' }}>
<img
Expand Down
28 changes: 28 additions & 0 deletions src/app/_components/PostSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Blogpost } from '../../../payload/payload-types'
import { estimateReadTime } from '../../_utilities/estimateReadTime'
import { formatDateTime } from '../../_utilities/formatDateTime'
import AuthorPill from '../AuthorPill'
import ArchiveButton from '../BlogPostArchiveButton'

export default function PostSummary({ post }: { post: Blogpost }) {
const { title, publishedAt, content, authors } = post

return (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{/* Left column */}
<div style={{ flex: 1 }}>
<ArchiveButton collection="blogposts" />
<h5>{title}</h5>
<div>
{formatDateTime(publishedAt)} | {estimateReadTime('Placeholder')}
</div>
</div>

{/* Right column */}
<div style={{ flex: 1, textAlign: 'right' }}>
<p style={{ fontSize: 20 }}>WRITTEN BY</p>
<AuthorPill author={post.authors[0]} />
</div>
</div>
)
}
10 changes: 7 additions & 3 deletions src/app/_graphql/blogposts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MEDIA_FIELDS } from "./media"
import { MEDIA_FIELDS } from './media'

export const BLOGPOST = `
query Blogpost {
Blogposts {
query Blogpost ($slug: String) {
Blogposts(where: { slug: { equals: $slug }}) {
docs {
id
title
Expand All @@ -15,6 +15,9 @@ export const BLOGPOST = `
authors {
name
role
featuredImage {
${MEDIA_FIELDS}
}
}
categories {
title
Expand All @@ -28,6 +31,7 @@ export const BLOGPOST = `
featuredImage {
${MEDIA_FIELDS}
}
content_html
createdAt
updatedAt
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/_graphql/contentFromAuthor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MEDIA_FIELDS } from "@/app/_graphql/media";
import { MEDIA_FIELDS } from '@/app/_graphql/media'

export const CONTENT_FROM_AUTHOR = `
query GetContent($authorID: JSON!) {
Expand Down
7 changes: 7 additions & 0 deletions src/app/_utilities/estimateReadTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function estimateReadTime(text: string): string {
const WPM = 250
const wordCount = text.split(/\s+/).length
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this could be kept on the CMS, through an onUpdate hook or something. Because splitting a string might be costly when the string is an entire blogpost. But it's ok for now 👍

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted! will make this an issue

const readTimeMinutes = Math.ceil(wordCount / WPM)

return `${readTimeMinutes} min read`
}
2 changes: 1 addition & 1 deletion src/app/_utilities/useEpisodeDuration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'

import { formatEpisodeDuration } from "./formatEpisodeDuration"
import { formatEpisodeDuration } from './formatEpisodeDuration'

export function useEpisodeDuration({ src }: { src: string }): string {
const [duration, setDuration] = useState<number | null>(null)
Expand Down
5 changes: 5 additions & 0 deletions src/payload/collections/BlogPosts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HTMLConverterFeature, lexicalEditor, lexicalHTML } from '@payloadcms/richtext-lexical'
import type { CollectionConfig } from 'payload/types'

import { admins } from '../access/admins'
Expand Down Expand Up @@ -40,7 +41,11 @@ export const BlogPosts: CollectionConfig = {
label: 'Content',
type: 'richText',
required: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures, HTMLConverterFeature({})],
}),
},
lexicalHTML('content', { name: 'content_html' }),
{
name: 'featuredImage',
label: 'Featured Image',
Expand Down
Loading