Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for nested web pages
Browse files Browse the repository at this point in the history
jordanarldt committed Jan 22, 2025

Unverified

This user has not yet uploaded their public signing key.
1 parent da2a462 commit b815221
Showing 18 changed files with 304 additions and 125 deletions.
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { getSessionCustomerAccessToken } from '~/auth';
import { client } from '~/client';
import { graphql, VariablesOf } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { BreadcrumbsFragment } from '~/components/breadcrumbs/fragment';
import { BreadcrumbsCategoryFragment } from '~/components/breadcrumbs/fragment';

const CategoryPageQuery = graphql(
`
@@ -38,7 +38,7 @@ const CategoryPageQuery = graphql(
}
}
`,
[BreadcrumbsFragment],
[BreadcrumbsCategoryFragment],
);

type Variables = VariablesOf<typeof CategoryPageQuery>;
24 changes: 15 additions & 9 deletions core/app/[locale]/(default)/account/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { PropsWithChildren } from 'react';

import { AccountLayout } from '@/vibes/soul/sections/account-layout';
import { SidebarMenu } from '@/vibes/soul/sections/sidebar-menu';
import { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';
import { auth } from '~/auth';
import { redirect } from '~/i18n/routing';

@@ -22,15 +23,20 @@ export default async function Layout({ children, params }: Props) {
}

return (
<AccountLayout
links={[
{ href: '/account/orders', label: t('orders') },
{ href: '/account/addresses', label: t('addresses') },
{ href: '/account/settings', label: t('settings') },
{ href: '/logout', label: t('logout') },
]}
<StickySidebarLayout
sidebar={
<SidebarMenu
links={[
{ href: '/account/orders', label: t('orders') },
{ href: '/account/addresses', label: t('addresses') },
{ href: '/account/settings', label: t('settings') },
{ href: '/logout', label: t('logout') },
]}
/>
}
sidebarSize="small"
>
{children}
</AccountLayout>
</StickySidebarLayout>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { clsx } from 'clsx';

import { Stream, Streamable } from '@/vibes/soul/lib/streamable';
import { Breadcrumb, Breadcrumbs, BreadcrumbsSkeleton } from '@/vibes/soul/primitives/breadcrumbs';
import { SectionLayout } from '@/vibes/soul/sections/section-layout';

export interface WebPage {
title: string;
content: string;
breadcrumbs: Breadcrumb[];
seo: {
pageTitle: string;
metaDescription: string;
@@ -17,18 +15,12 @@ export interface WebPage {
interface Props {
webPage: Streamable<WebPage>;
breadcrumbs?: Streamable<Breadcrumb[]>;
className?: string;
children?: React.ReactNode;
}

export function WebPageContent({
webPage: streamableWebPage,
className = '',
breadcrumbs,
children,
}: Props) {
export function WebPageContent({ webPage: streamableWebPage, breadcrumbs, children }: Props) {
return (
<SectionLayout className={clsx('mx-auto w-full max-w-4xl', className)}>
<section className="w-full max-w-4xl">
<Stream fallback={<WebPageContentSkeleton />} value={streamableWebPage}>
{(webPage) => {
const { title, content } = webPage;
@@ -52,7 +44,7 @@ export function WebPageContent({
);
}}
</Stream>
</SectionLayout>
</section>
);
}

@@ -78,7 +70,7 @@ function WebPageBodySkeleton() {
);
}

export function WebPageContentSkeleton() {
function WebPageContentSkeleton() {
return (
<div>
<div className="mx-auto w-full max-w-4xl pb-8 @2xl:pb-12 @4xl:pb-16">
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use server';

import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';
import { BigCommerceAPIError } from '@bigcommerce/catalyst-client';
import { SubmissionResult } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { getLocale, getTranslations } from 'next-intl/server';
@@ -96,18 +96,9 @@ export async function submitContactForm<F extends Field>(
// eslint-disable-next-line no-console
console.error(error);

if (error instanceof BigCommerceGQLError) {
if (error instanceof BigCommerceAPIError) {
return {
lastResult: submission.reply({
formErrors: error.errors.map(({ message }) => message),
}),
fields: prevState.fields,
};
}

if (error instanceof Error) {
return {
lastResult: submission.reply({ formErrors: [error.message] }),
lastResult: submission.reply({ formErrors: [t('Errors.apiError')] }),
fields: prevState.fields,
};
}
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import { cache } from 'react';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { BreadcrumbsWebPageFragment } from '~/components/breadcrumbs/fragment';

const ContactPageQuery = graphql(
`
@@ -12,6 +13,7 @@ const ContactPageQuery = graphql(
... on ContactPage {
entityId
name
...BreadcrumbsFragment
path
contactFields
htmlBody
@@ -32,7 +34,7 @@ const ContactPageQuery = graphql(
}
}
`,
[],
[BreadcrumbsWebPageFragment],
);

export const getWebpageData = cache(async (variables: { id: string }) => {
Original file line number Diff line number Diff line change
@@ -6,8 +6,12 @@ import { Breadcrumb } from '@/vibes/soul/primitives/breadcrumbs';
import { ButtonLink } from '@/vibes/soul/primitives/button-link';
import { DynamicForm } from '@/vibes/soul/primitives/dynamic-form';
import type { Field, FieldGroup } from '@/vibes/soul/primitives/dynamic-form/schema';
import {
breadcrumbsTransformer,
truncateBreadcrumbs,
} from '~/data-transformers/breadcrumbs-transformer';

import { WebPage, WebPageContent } from '../../_components/web-page';
import { WebPage, WebPageContent } from '../_components/web-page';

import { submitContactForm } from './_actions/submit-contact-form';
import { getWebpageData } from './page-data';
@@ -46,10 +50,13 @@ async function getWebPage(id: string): Promise<ContactPage> {
return notFound();
}

const breadcrumbs = breadcrumbsTransformer(webpage.breadcrumbs);

return {
entityId: webpage.entityId,
title: webpage.name,
path: webpage.path,
breadcrumbs,
content: webpage.htmlBody,
contactFields: webpage.contactFields,
seo: webpage.seo,
@@ -59,17 +66,20 @@ async function getWebPage(id: string): Promise<ContactPage> {

async function getWebPageBreadcrumbs(id: string): Promise<Breadcrumb[]> {
const webpage = await getWebPage(id);

return [
const [, ...rest] = webpage.breadcrumbs.reverse();
const breadcrumbs = [
{
label: 'Home',
href: '/',
},
...rest.reverse(),
{
label: webpage.title,
href: '#',
},
];

return truncateBreadcrumbs(breadcrumbs, 5);
}

async function getWebPageWithSuccessContent(id: string, message: string) {
88 changes: 88 additions & 0 deletions core/app/[locale]/(default)/webpages/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
import { cache } from 'react';

import { SidebarMenu } from '@/vibes/soul/sections/sidebar-menu';
import { StickySidebarLayout } from '@/vibes/soul/sections/sticky-sidebar-layout';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';

interface Props extends React.PropsWithChildren {
params: Promise<{ id: string }>;
}

const WebPageChildrenQuery = graphql(`
query WebPageChildren($id: ID!) {
node(id: $id) {
... on WebPage {
children(first: 20) {
edges {
node {
name
... on NormalPage {
path
}
... on ContactPage {
path
}
... on RawHtmlPage {
path
}
... on ExternalLinkPage {
link
}
}
}
}
}
}
}
`);

interface PageLink {
label: string;
href: string;
}

const getWebPageChildren = cache(async (id: string): Promise<PageLink[]> => {
const { data } = await client.fetch({
document: WebPageChildrenQuery,
variables: { id: decodeURIComponent(id) },
fetchOptions: { next: { revalidate } },
});

if (!data.node) {
return [];
}

if (!('children' in data.node)) {
return [];
}

const { children } = data.node;

return removeEdgesAndNodes(children).reduce((acc: PageLink[], child) => {
if ('path' in child) {
return [...acc, { label: child.name, href: child.path }];
}

if ('link' in child) {
return [...acc, { label: child.name, href: child.link }];
}

return acc;
}, []);
});

export default async function WebPageLayout({ params, children }: Props) {
const { id } = await params;

return (
<StickySidebarLayout
sidebar={<SidebarMenu links={getWebPageChildren(id)} />}
sidebarSize="small"
>
{children}
</StickySidebarLayout>
);
}
38 changes: 38 additions & 0 deletions core/app/[locale]/(default)/webpages/[id]/normal/page-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { cache } from 'react';

import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { BreadcrumbsWebPageFragment } from '~/components/breadcrumbs/fragment';

const NormalPageQuery = graphql(
`
query NormalPageQuery($id: ID!) {
node(id: $id) {
... on NormalPage {
__typename
name
...BreadcrumbsFragment
htmlBody
entityId
seo {
pageTitle
metaDescription
metaKeywords
}
}
}
}
`,
[BreadcrumbsWebPageFragment],
);

export const getWebpageData = cache(async (variables: { id: string }) => {
const { data } = await client.fetch({
document: NormalPageQuery,
variables,
fetchOptions: { next: { revalidate } },
});

return data;
});
Original file line number Diff line number Diff line change
@@ -2,8 +2,12 @@ import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

import { Breadcrumb } from '@/vibes/soul/primitives/breadcrumbs';
import {
breadcrumbsTransformer,
truncateBreadcrumbs,
} from '~/data-transformers/breadcrumbs-transformer';

import { WebPageContent, WebPage as WebPageData } from '../../_components/web-page';
import { WebPageContent, WebPage as WebPageData } from '../_components/web-page';

import { getWebpageData } from './page-data';

@@ -19,26 +23,32 @@ async function getWebPage(id: string): Promise<WebPageData> {
return notFound();
}

const breadcrumbs = breadcrumbsTransformer(webpage.breadcrumbs);

return {
title: webpage.name,
breadcrumbs,
content: webpage.htmlBody,
seo: webpage.seo,
};
}

async function getWebPageBreadcrumbs(id: string): Promise<Breadcrumb[]> {
const webpage = await getWebPage(id);

return [
const [, ...rest] = webpage.breadcrumbs.reverse();
const breadcrumbs = [
{
label: 'Home',
href: '/',
},
...rest.reverse(),
{
label: webpage.title,
href: '#',
},
];

return truncateBreadcrumbs(breadcrumbs, 5);
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
33 changes: 0 additions & 33 deletions core/app/[locale]/(default)/webpages/normal/[id]/page-data.ts

This file was deleted.

15 changes: 14 additions & 1 deletion core/components/breadcrumbs/fragment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { graphql } from '~/client/graphql';

export const BreadcrumbsFragment = graphql(`
export const BreadcrumbsCategoryFragment = graphql(`
fragment BreadcrumbsFragment on Category {
breadcrumbs(depth: 5) {
edges {
@@ -12,3 +12,16 @@ export const BreadcrumbsFragment = graphql(`
}
}
`);

export const BreadcrumbsWebPageFragment = graphql(`
fragment BreadcrumbsFragment on WebPage {
breadcrumbs(depth: 8) {
edges {
node {
name
path
}
}
}
}
`);
2 changes: 1 addition & 1 deletion core/components/footer/fragment.ts
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ export const FooterFragment = graphql(`
}
}
content {
pages(filters: { isVisibleInNavigation: true }) {
pages(filters: { parentEntityIds: [0] }) {
edges {
node {
__typename
41 changes: 41 additions & 0 deletions core/data-transformers/breadcrumbs-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
import { ResultOf } from 'gql.tada';

import {
BreadcrumbsCategoryFragment,
BreadcrumbsWebPageFragment,
} from '~/components/breadcrumbs/fragment';
import { Breadcrumb } from '~/vibes/soul/primitives/breadcrumbs';

type BreadcrumbsResult =
| ResultOf<typeof BreadcrumbsWebPageFragment>
| ResultOf<typeof BreadcrumbsCategoryFragment>;

export const breadcrumbsTransformer = (breadcrumbs: BreadcrumbsResult['breadcrumbs']) => {
return removeEdgesAndNodes(breadcrumbs).reduce<Breadcrumb[]>((acc, crumb) => {
if (crumb.path) {
return [...acc, { label: crumb.name, href: crumb.path }];
}

return acc;
}, []);
};

export function truncateBreadcrumbs(breadcrumbs: Breadcrumb[], length: number): Breadcrumb[] {
if (breadcrumbs.length < length) {
return breadcrumbs;
}

const middleIndex = Math.floor(breadcrumbs.length / 2);
const dropCount = breadcrumbs.length - length;
const dropEach = Math.ceil(dropCount / 2);
const dropLast = Math.floor(dropCount / 2);
const [first, last] = [
breadcrumbs.slice(0, middleIndex - dropEach),
breadcrumbs.slice(middleIndex + dropLast),
];

last[0] = { label: '...', href: '#' };

return [...first, ...last];
}
4 changes: 2 additions & 2 deletions core/middlewares/with-routes.ts
Original file line number Diff line number Diff line change
@@ -318,12 +318,12 @@ export const withRoutes: MiddlewareFactory = () => {
}

case 'NormalPage': {
url = `/${locale}/webpages/normal/${node.id}`;
url = `/${locale}/webpages/${node.id}/normal/`;
break;
}

case 'ContactPage': {
url = `/${locale}/webpages/contact/${node.id}`;
url = `/${locale}/webpages/${node.id}/contact/`;
break;
}

38 changes: 0 additions & 38 deletions core/vibes/soul/sections/account-layout/index.tsx

This file was deleted.

63 changes: 63 additions & 0 deletions core/vibes/soul/sections/sidebar-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Stream, Streamable } from '@/vibes/soul/lib/streamable';

import { SidebarMenuLink } from './sidebar-menu-link';
import { SidebarMenuSelect } from './sidebar-menu-select';

interface MenuLink {
href: string;
label: string;
}

interface Props {
links: Streamable<MenuLink[]>;
placeholderCount?: number;
}

export function SidebarMenu({ links: streamableLinks, placeholderCount = 5 }: Props) {
return (
<Stream
fallback={<SidebarMenuSkeleton placeholderCount={placeholderCount} />}
value={streamableLinks}
>
{(links) => {
if (!links.length) {
return null;
}

return (
<nav>
<ul className="hidden @2xl:block">
{links.map((link, index) => (
<li key={index}>
<SidebarMenuLink href={link.href}>{link.label}</SidebarMenuLink>
</li>
))}
</ul>
<div className="@2xl:hidden">
<SidebarMenuSelect links={links} />
</div>
</nav>
);
}}
</Stream>
);
}

function SidebarMenuSkeleton({ placeholderCount }: { placeholderCount: number }) {
return (
<>
<div className="hidden [mask-image:linear-gradient(to_bottom,_black_0%,_transparent_90%)] @2xl:block">
<div className="w-full animate-pulse">
{Array.from({ length: placeholderCount }).map((_, index) => (
<div className="flex h-10 items-center px-3" key={index}>
<div className="h-[1lh] flex-1 rounded-lg bg-contrast-100" />
</div>
))}
</div>
</div>
<div className="@2xl:hidden">
<div className="h-[50px] w-full rounded-lg bg-contrast-100" />
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import React from 'react';
import { Link } from '~/components/link';
import { usePathname } from '~/i18n/routing';

export function AccountLayoutLink({
export function SidebarMenuLink({
className,
href,
...rest
Original file line number Diff line number Diff line change
@@ -3,17 +3,13 @@
import { Select } from '@/vibes/soul/form/select';
import { usePathname, useRouter } from '~/i18n/routing';

export function AccountLayoutLinkSelect({
links,
}: {
links: Array<{ href: string; label: string }>;
}) {
export function SidebarMenuSelect({ links }: { links: Array<{ href: string; label: string }> }) {
const pathname = usePathname();
const router = useRouter();

return (
<Select
name="account-layout-link-select"
name="sidebar-layout-link-select"
onValueChange={(value) => {
router.push(value);
}}

0 comments on commit b815221

Please sign in to comment.