Skip to content

Commit

Permalink
Merge pull request #58 from oxen-io/sanity_cms_schemas_and_components
Browse files Browse the repository at this point in the history
Sanity Portable Text & Site Schemas
  • Loading branch information
Aerilym authored Oct 10, 2024
2 parents 7476324 + 582716f commit 11b5a76
Show file tree
Hide file tree
Showing 132 changed files with 5,269 additions and 244 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,13 @@ jobs:
run: |
grep '^export ' ./apps/staking/scripts/mock-build-env.sh | sed 's/export //' >> $GITHUB_ENV
- name: Set environment variables from github secrets
run: |
echo "NEXT_PUBLIC_SANITY_DATASET=${{ secrets.NEXT_PUBLIC_SANITY_DATASET }}" >> $GITHUB_ENV
echo "NEXT_PUBLIC_SANITY_PROJECT_ID=${{ secrets.NEXT_PUBLIC_SANITY_PROJECT_ID }}" >> $GITHUB_ENV
echo "SANITY_API_READ_TOKEN=${{ secrets.SANITY_API_READ_TOKEN }}" >> $GITHUB_ENV
echo "SANITY_REVALIDATE_SECRET=${{ secrets.SANITY_REVALIDATE_SECRET }}" >> $GITHUB_ENV
echo "NEXT_PUBLIC_SANITY_API_VERSION=${{ secrets.NEXT_PUBLIC_SANITY_API_VERSION }}" >> $GITHUB_ENV
- name: Run builds
run: pnpm build
6 changes: 6 additions & 0 deletions apps/foundation/.env.local.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NEXT_PUBLIC_ENV_FLAG= pick from dev, qa, stg, prd
NEXT_PUBLIC_SANITY_DATASET=
NEXT_PUBLIC_SANITY_PROJECT_ID=
SANITY_API_READ_TOKEN=
SANITY_REVALIDATE_SECRET=
NEXT_PUBLIC_SANITY_API_VERSION="2024-09-30"
9 changes: 9 additions & 0 deletions apps/foundation/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ['@session/eslint-config/next.js'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
},
};
13 changes: 13 additions & 0 deletions apps/foundation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Session Technology Foundation Website

The Session Technology Foundation Website is a [Next.js](https://nextjs.org/) app.

## Getting Started

You can follow the generic instructions in the root [README.md](../../README.md#getting-started) to get started.

## Development

Running the app requires several environment variables to be set. See the [.env.local.template](.env.local.template)
file for a list
of required variables.
10 changes: 10 additions & 0 deletions apps/foundation/app/(Sanity)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';
import '@session/ui/styles';

export default function SanityLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
8 changes: 8 additions & 0 deletions apps/foundation/app/(Sanity)/studio/[[...tool]]/Studio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';

import SanityStudio from '@session/sanity-cms/components/SanityStudio';
import { sanityConfig } from '@/lib/sanity/sanity.config';

export default function Studio() {
return <SanityStudio config={sanityConfig} />;
}
7 changes: 7 additions & 0 deletions apps/foundation/app/(Sanity)/studio/[[...tool]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Loading } from '@session/ui/components/loading';
import { SanityStudioSSRPage } from '@session/sanity-cms/components/SanityStudioSSRPage';
import Studio from '@/app/(Sanity)/studio/[[...tool]]/Studio';

export default function StudioPage() {
return <SanityStudioSSRPage sanityStudio={<Studio />} suspenseFallback={<Loading />} />;
}
13 changes: 13 additions & 0 deletions apps/foundation/app/(Site)/[slug]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ReactNode } from 'react';
import { Footer } from '@/components/Footer';
import { getInitialSiteDataForSSR } from '@/lib/sanity/sanity-server';

export default async function UniversalPageLayout({ children }: { children: ReactNode }) {
const { settings } = await getInitialSiteDataForSSR();
return (
<>
{children}
<Footer className="max-w-screen-md" {...settings} />
</>
);
}
64 changes: 64 additions & 0 deletions apps/foundation/app/(Site)/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getPageBySlug } from '@session/sanity-cms/queries/getPage';
import { client } from '@/lib/sanity/sanity.client';
import { getPagesSlugs } from '@session/sanity-cms/queries/getPages';
import { notFound } from 'next/navigation';
import { getLandingPageSlug } from '@/lib/sanity/sanity-server';
import PortableText from '@/components/PortableText';
import logger from '@/lib/logger';
import { NEXTJS_EXPLICIT_IGNORED_ROUTES, NEXTJS_IGNORED_PATTERNS } from '@/lib/constants';

/**
* 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 pages = await getPagesSlugs({ client });
const slugs = new Set(pages.map((page) => page.slug.current));

const landingPageSlug = await getLandingPageSlug();
if (landingPageSlug) {
slugs.delete(landingPageSlug);
} else {
console.warn('No landing page set in settings to statically generate');
}

const pagesToGenerate = Array.from(slugs);
logger.info(`Generating static params for ${pagesToGenerate.length} pages`);
logger.info(pagesToGenerate);
return pagesToGenerate;
}

type PageProps = {
params: { slug?: string };
};

export default async function UniversalPage({ params }: PageProps) {
const slug = params.slug;
if (!slug) return notFound();

if (
NEXTJS_EXPLICIT_IGNORED_ROUTES.includes(slug) ||
NEXTJS_IGNORED_PATTERNS.some((pattern) => slug.includes(pattern))
) {
return;
}

logger.info(`Generating page for slug ${slug}`);

const page = await getPageBySlug({
client,
slug,
});

if (!page) return notFound();

return <PortableText body={page.body} className="max-w-screen-md" wrapperComponent="main" />;
}
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 justify-center 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 }), 'w-full text-wrap 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>
);
}
Loading

0 comments on commit 11b5a76

Please sign in to comment.