-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import PortableText from '@/components/PortableText'; | ||
import { getLocale, getTranslations } from 'next-intl/server'; | ||
import { Button } from '@session/ui/ui/button'; | ||
import { cn } from '@session/ui/lib/utils'; | ||
import { getLangDir } from 'rtl-detect'; | ||
import Link from 'next/link'; | ||
import { ButtonDataTestId } from '@/testing/data-test-ids'; | ||
import { SANITY_SCHEMA_URL } from '@/lib/constants'; | ||
import type { FormattedPostType } from '@session/sanity-cms/queries/getPost'; | ||
import { notFound } from 'next/navigation'; | ||
import logger from '@/lib/logger'; | ||
import PostInfoBlock from '@/app/(Site)/blog/[slug]/PostInfoBlock'; | ||
import HeadingOutline from '@/app/(Site)/blog/[slug]/HeadingOutline'; | ||
|
||
export type PostProps = { | ||
post: FormattedPostType; | ||
}; | ||
|
||
export default async function BlogPost({ post }: PostProps) { | ||
const blogDictionary = await getTranslations('blog'); | ||
const locale = await getLocale(); | ||
const direction = getLangDir(locale); | ||
|
||
const body = post.body; | ||
|
||
if (!body) { | ||
logger.error(`No body found for post: ${post.slug}`); | ||
return notFound(); | ||
} | ||
|
||
const allH2s = body.filter((block) => block._type === 'block' && block.style === 'h2'); | ||
|
||
const headings: Array<string> = allH2s | ||
.map((block) => | ||
'children' in block && Array.isArray(block.children) ? block.children[0].text : null | ||
) | ||
.filter(Boolean); | ||
|
||
return ( | ||
<article className="mx-auto mb-32 mt-4 flex max-w-screen-xl flex-col"> | ||
<Link href={SANITY_SCHEMA_URL.POST} prefetch> | ||
<Button | ||
data-testid={ButtonDataTestId.Back_To_Blog} | ||
className={cn('text-session-text-black-secondary my-2 gap-2 fill-current px-1')} | ||
size="sm" | ||
rounded="md" | ||
variant="ghost" | ||
> | ||
<span className={cn(direction === 'rtl' && 'rotate-180')}>←</span> | ||
{blogDictionary('backToBlog')} | ||
</Button> | ||
</Link> | ||
<PostInfoBlock | ||
className="w-full" | ||
postInfo={post} | ||
renderWithPriority | ||
mobileImagePosition="below" | ||
/> | ||
<div className="mt-6 flex flex-row gap-12 md:mt-12"> | ||
<PortableText body={body} className="max-w-screen-md" wrapperComponent="section" /> | ||
{headings.length ? ( | ||
<HeadingOutline headings={headings} title={blogDictionary('inThisArticle')} /> | ||
) : null} | ||
</div> | ||
</article> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
'use client'; | ||
|
||
import Typography from '@session/ui/components/Typography'; | ||
import { cn } from '@session/ui/lib/utils'; | ||
import { navlinkVariants } from '@session/ui/components/NavLink'; | ||
|
||
function scrollToHeading(text: string) { | ||
document.querySelectorAll('h2').forEach((heading) => { | ||
if (text && heading.textContent && heading.textContent === text) { | ||
heading.scrollIntoView({ | ||
behavior: 'smooth', | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
type HeadingOutlineProps = { | ||
headings: Array<string>; | ||
title: string; | ||
}; | ||
|
||
export default function HeadingOutline({ title, headings }: HeadingOutlineProps) { | ||
return ( | ||
<nav className="wrap sticky top-12 mt-7 hidden h-max w-max max-w-[25vw] lg:block"> | ||
<Typography variant="h2" className="mb-3"> | ||
{title} | ||
</Typography> | ||
<ul className="text-session-text-black-secondary flex flex-col gap-2"> | ||
{headings.map((heading) => ( | ||
<li key={`scroll-to-${heading}`}> | ||
<button | ||
onClick={() => { | ||
scrollToHeading(heading); | ||
}} | ||
className={cn(navlinkVariants({ active: false }), 'text-start')} | ||
> | ||
{heading} | ||
</button> | ||
</li> | ||
))} | ||
</ul> | ||
</nav> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { SanityImage } from '@session/sanity-cms/components/SanityImage'; | ||
import { client } from '@/lib/sanity/sanity.client'; | ||
import Typography from '@session/ui/components/Typography'; | ||
import { getLocale } from 'next-intl/server'; | ||
import { cn } from '@session/ui/lib/utils'; | ||
import { safeTry } from '@session/util-js/try'; | ||
import logger from '@/lib/logger'; | ||
import type { FormattedPostType } from '@session/sanity-cms/queries/getPost'; | ||
import type { ReactNode } from 'react'; | ||
|
||
const getLocalizedPosedDate = async (date: Date) => { | ||
const locale = await getLocale(); | ||
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long', day: 'numeric' }).format( | ||
date | ||
); | ||
}; | ||
export type PostBlockProps = { | ||
postInfo: Pick<FormattedPostType, 'title' | 'summary' | 'featuredImage' | 'author' | 'date'>; | ||
renderWithPriority?: boolean; | ||
mobileImagePosition?: 'above' | 'below'; | ||
columnAlways?: boolean; | ||
className?: string; | ||
children?: ReactNode; | ||
}; | ||
|
||
export default async function PostInfoBlock({ | ||
postInfo, | ||
renderWithPriority, | ||
mobileImagePosition = 'above', | ||
columnAlways, | ||
className, | ||
children, | ||
}: PostBlockProps) { | ||
const { title, summary, featuredImage, author, date } = postInfo; | ||
|
||
let localizedPublishedAt = null; | ||
if (date) { | ||
const [err, res] = await safeTry(getLocalizedPosedDate(date)); | ||
if (err) { | ||
logger.error(err); | ||
localizedPublishedAt = date.toLocaleDateString(); | ||
} else { | ||
localizedPublishedAt = res; | ||
} | ||
} | ||
|
||
return ( | ||
<div | ||
className={cn( | ||
'flex w-full items-center gap-8', | ||
columnAlways ? 'flex-col' : 'md:grid md:grid-cols-2', | ||
mobileImagePosition === 'below' ? 'flex-col-reverse' : 'flex-col', | ||
className | ||
)} | ||
> | ||
<div className="aspect-video w-full overflow-hidden rounded-lg"> | ||
<SanityImage | ||
className="h-full" | ||
client={client} | ||
value={featuredImage} | ||
cover | ||
renderWithPriority={renderWithPriority} | ||
/> | ||
</div> | ||
<div className="flex w-full flex-col gap-2"> | ||
<Typography variant={columnAlways ? 'h2' : 'h1'} className="w-full"> | ||
{title} | ||
</Typography> | ||
<span className="text-session-text-black-secondary inline-flex w-full gap-1"> | ||
{date ? <time dateTime={date.toISOString()}>{localizedPublishedAt}</time> : null} | ||
{date ? '/' : null} | ||
{author?.name ? <address>{author.name}</address> : null} | ||
</span> | ||
{summary ? <p className="w-full">{summary}</p> : null} | ||
{children} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { client } from '@/lib/sanity/sanity.client'; | ||
import { notFound } from 'next/navigation'; | ||
import { getPostsSlugs } from '@session/sanity-cms/queries/getPosts'; | ||
import { getPostBySlug } from '@session/sanity-cms/queries/getPost'; | ||
import logger from '@/lib/logger'; | ||
import BlogPost from '@/app/(Site)/blog/[slug]/BlogPost'; | ||
|
||
/** | ||
* Force static rendering and cache the data of a layout or page by forcing `cookies()`, `headers()` | ||
* and `useSearchParams()` to return empty values. | ||
* @see {@link https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic} | ||
*/ | ||
export const dynamic = 'force-static'; | ||
/** | ||
* Dynamic segments not included in generateStaticParams are generated on demand. | ||
* @see {@link https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams} | ||
*/ | ||
export const dynamicParams = true; | ||
|
||
export async function generateStaticParams() { | ||
const posts = await getPostsSlugs({ client }); | ||
const slugs = new Set(posts.map((post) => post.slug.current)); | ||
|
||
if (slugs.size === 0) { | ||
console.warn('No posts found. Not statically generating any posts.'); | ||
} | ||
|
||
const postsToGenerate = Array.from(slugs); | ||
logger.info(`Generating static params for ${postsToGenerate.length} posts`); | ||
logger.info(postsToGenerate); | ||
return postsToGenerate; | ||
} | ||
|
||
type PageProps = { | ||
params: { slug?: string }; | ||
}; | ||
|
||
export default async function PostPage({ params }: PageProps) { | ||
const slug = params.slug; | ||
|
||
if (!slug) { | ||
logger.warn( | ||
"No slug provided for post page, this means next.js couldn't find the post home page. Returning not found" | ||
); | ||
return notFound(); | ||
} | ||
|
||
logger.info(`Generating page for slug ${slug}`); | ||
|
||
const post = await getPostBySlug({ | ||
client, | ||
slug, | ||
}); | ||
|
||
if (!post) return notFound(); | ||
|
||
return <BlogPost post={post} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import PostInfoBlock from '@/app/(Site)/blog/[slug]/PostInfoBlock'; | ||
import { client } from '@/lib/sanity/sanity.client'; | ||
import { getPostsWithMetadata } from '@session/sanity-cms/queries/getPosts'; | ||
import logger from '@/lib/logger'; | ||
import { notFound } from 'next/navigation'; | ||
import Link from 'next/link'; | ||
import { BLOG, SANITY_SCHEMA_URL } from '@/lib/constants'; | ||
import { cn } from '@session/ui/lib/utils'; | ||
import { getTranslations } from 'next-intl/server'; | ||
import Typography from '@session/ui/components/Typography'; | ||
|
||
/** | ||
* Force static rendering and cache the data of a layout or page by forcing `cookies()`, `headers()` | ||
* and `useSearchParams()` to return empty values. | ||
* @see {@link https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic} | ||
*/ | ||
export const dynamic = 'force-static'; | ||
|
||
async function ReadMoreText() { | ||
const blogDictionary = await getTranslations('blog'); | ||
return ( | ||
<span className="group-hover:border-b-session-green group-hover:text-session-text-black hover:border-b-session-green mt-1 w-max border-b-2 border-b-transparent text-sm"> | ||
{blogDictionary('readMore')} | ||
</span> | ||
); | ||
} | ||
|
||
export default async function BlogGridPage() { | ||
const [latestPost, ...rest] = await getPostsWithMetadata({ client }); | ||
|
||
const blogDictionary = await getTranslations('blog'); | ||
|
||
if (!latestPost) { | ||
logger.error('No latest post found'); | ||
return notFound(); | ||
} | ||
|
||
const linkClassName = cn( | ||
'group', | ||
'transition-all duration-300 ease-in-out motion-reduce:transition-none', | ||
'[&_*]:transition-all [&_*]:duration-300 [&_*]:ease-in-out [&_*]:motion-reduce:transition-none', | ||
'[&_img]:hover:brightness-125 [&_img]:hover:saturate-150 [&_h1]:hover:text-session-green-dark [&_h2]:hover:text-session-green-dark' | ||
); | ||
|
||
return ( | ||
<main className="mx-auto mt-4 flex max-w-screen-xl flex-col"> | ||
<Link | ||
href={`${SANITY_SCHEMA_URL.POST}${latestPost.slug.current}`} | ||
prefetch | ||
className={linkClassName} | ||
> | ||
<PostInfoBlock postInfo={latestPost} renderWithPriority> | ||
<ReadMoreText /> | ||
</PostInfoBlock> | ||
</Link> | ||
<Typography variant="h2" className="mt-12"> | ||
{blogDictionary('morePosts')} | ||
</Typography> | ||
<div className="mt-4 grid grid-cols-1 gap-12 md:mt-8 md:grid-cols-3"> | ||
{rest.map((post, index) => ( | ||
<Link | ||
key={`post-list-${post.slug.current}`} | ||
href={`${SANITY_SCHEMA_URL.POST}${post.slug.current}`} | ||
prefetch={index < BLOG.POSTS_TO_PREFETCH} | ||
className={linkClassName} | ||
> | ||
<PostInfoBlock postInfo={post} renderWithPriority columnAlways> | ||
<ReadMoreText /> | ||
</PostInfoBlock> | ||
</Link> | ||
))} | ||
</div> | ||
</main> | ||
); | ||
} |