Skip to content

Commit

Permalink
feat: create foundation site blog
Browse files Browse the repository at this point in the history
  • Loading branch information
Aerilym committed Oct 7, 2024
1 parent bc90e94 commit e31f3cf
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 0 deletions.
67 changes: 67 additions & 0 deletions apps/foundation/app/(Site)/blog/[slug]/BlogPost.tsx
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>
);
}
44 changes: 44 additions & 0 deletions apps/foundation/app/(Site)/blog/[slug]/HeadingOutline.tsx
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>
);
}
79 changes: 79 additions & 0 deletions apps/foundation/app/(Site)/blog/[slug]/PostInfoBlock.tsx
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>
);
}
58 changes: 58 additions & 0 deletions apps/foundation/app/(Site)/blog/[slug]/page.tsx
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} />;
}
75 changes: 75 additions & 0 deletions apps/foundation/app/(Site)/blog/page.tsx
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>
);
}

0 comments on commit e31f3cf

Please sign in to comment.