-
Notifications
You must be signed in to change notification settings - Fork 1
scaffold blogpost content page #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7bae8f0
1cf2c6d
4eb0839
ed0c398
a969bf3
6921ee5
04ec246
a5ce6f1
2393b0b
498dec7
b7f226f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ) | ||
| } |
| 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> | ||
| ) | ||
| } |
| 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> | ||
| ) | ||
| } |
| 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) | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <a href={`/${collection}`}>{formatArchiveButton(collection)}</a> | ||
| </div> | ||
| ) | ||
| } | ||
| 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> | ||
| ) | ||
| } |
| 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 }} /> | ||
| } |
| 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> | ||
| ) | ||
| } |
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 👍
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||
| } | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
collectionsThere was a problem hiding this comment.
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