diff --git a/.changeset/many-bikes-roll.md b/.changeset/many-bikes-roll.md new file mode 100644 index 000000000..4db020cdd --- /dev/null +++ b/.changeset/many-bikes-roll.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": patch +--- + +split contact us and normal websites into individual pages diff --git a/apps/core/app/[locale]/(default)/(webpages)/[page]/page.tsx b/apps/core/app/[locale]/(default)/(webpages)/[page]/page.tsx deleted file mode 100644 index ce3956b6a..000000000 --- a/apps/core/app/[locale]/(default)/(webpages)/[page]/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { NextIntlClientProvider } from 'next-intl'; -import { getMessages } from 'next-intl/server'; - -import { getReCaptchaSettings } from '~/client/queries/get-recaptcha-settings'; -import { getWebPage } from '~/client/queries/get-web-page'; -import { ContactUs } from '~/components/forms'; - -import { PageContent } from '../_components/page-content'; - -interface Props { - params: { page: string; locale: string }; -} - -export async function generateMetadata({ params }: Props): Promise { - const path = `/${params.page}`; - const webpage = await getWebPage({ path }); - - if (!webpage) { - notFound(); - } - - const { seo } = webpage; - - return { - title: seo.pageTitle, - description: seo.metaDescription, - keywords: seo.metaKeywords, - }; -} - -export default async function WebPage({ params: { locale, page } }: Props) { - const path = `/${page}`; - const webpage = await getWebPage({ path }); - - if (!webpage) { - notFound(); - } - - const messages = await getMessages({ locale }); - const { name, htmlBody, __typename: pageType, entityId } = webpage; - - switch (pageType) { - case 'ContactPage': { - const reCaptchaSettings = await getReCaptchaSettings(); - - return ( - <> - - - - - - ); - } - - case 'NormalPage': - default: - return ; - } -} - -export const runtime = 'edge'; diff --git a/apps/core/app/[locale]/(default)/(webpages)/_components/page-content.tsx b/apps/core/app/[locale]/(default)/(webpages)/_components/page-content.tsx deleted file mode 100644 index 10bcdf230..000000000 --- a/apps/core/app/[locale]/(default)/(webpages)/_components/page-content.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { cn } from '~/lib/utils'; - -interface Props { - className?: string; - title: string; - content: string; -} - -export const PageContent = ({ className, content, title }: Props) => { - return ( -
-

{title}

-
-
- ); -}; diff --git a/apps/core/components/forms/_actions/submit-contact-form.ts b/apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/_actions/submit-contact-form.ts similarity index 100% rename from apps/core/components/forms/_actions/submit-contact-form.ts rename to apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/_actions/submit-contact-form.ts diff --git a/apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/fragment.ts b/apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/fragment.ts new file mode 100644 index 000000000..a95641691 --- /dev/null +++ b/apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/fragment.ts @@ -0,0 +1,20 @@ +import { graphql } from '~/client/graphql'; + +export const ContactUsFragment = graphql(` + fragment ContactUsFragment on Query { + node(id: $id) { + ... on ContactPage { + entityId + contactFields + } + } + site { + settings { + reCaptcha { + isEnabledOnStorefront + siteKey + } + } + } + } +`); diff --git a/apps/core/components/forms/contact-us.tsx b/apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/index.tsx similarity index 94% rename from apps/core/components/forms/contact-us.tsx rename to apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/index.tsx index 1eea87c18..ac7f89b1e 100644 --- a/apps/core/components/forms/contact-us.tsx +++ b/apps/core/app/[locale]/(default)/webpages/contact/[id]/contact-us/index.tsx @@ -1,3 +1,5 @@ +'use client'; + import { Button } from '@bigcommerce/components/button'; import { Field, @@ -10,6 +12,7 @@ import { import { Input } from '@bigcommerce/components/input'; import { Message } from '@bigcommerce/components/message'; import { TextArea } from '@bigcommerce/components/text-area'; +import { type FragmentOf } from 'gql.tada'; import { Loader2 as Spinner } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { ChangeEvent, useRef, useState } from 'react'; @@ -17,21 +20,13 @@ import { useFormStatus } from 'react-dom'; import ReCaptcha from 'react-google-recaptcha'; import { submitContactForm } from './_actions/submit-contact-form'; +import { ContactUsFragment } from './fragment'; interface FormStatus { status: 'success' | 'error'; message: string; } -interface ContactUsProps { - fields: string[]; - pageEntityId: number; - reCaptchaSettings?: { - isEnabledOnStorefront: boolean; - siteKey: string; - }; -} - const fieldNameMapping = { fullname: 'fullNameLabel', companyname: 'companyNameLabel', @@ -69,7 +64,11 @@ const Submit = () => { ); }; -export const ContactUs = ({ fields, pageEntityId, reCaptchaSettings }: ContactUsProps) => { +interface Props { + data: FragmentOf; +} + +export const ContactUs = ({ data }: Props) => { const form = useRef(null); const [formStatus, setFormStatus] = useState(null); const [isTextFieldValid, setTextFieldValidation] = useState(true); @@ -80,6 +79,13 @@ export const ContactUs = ({ fields, pageEntityId, reCaptchaSettings }: ContactUs const t = useTranslations('AboutUs'); + if (data.node?.__typename !== 'ContactPage') { + return null; + } + + const { contactFields: fields, entityId: pageEntityId } = data.node; + const reCaptchaSettings = data.site.settings?.reCaptcha; + const onReCaptchaChange = (token: string | null) => { if (!token) { return setReCaptchaValid(false); diff --git a/apps/core/app/[locale]/(default)/webpages/contact/[id]/page.tsx b/apps/core/app/[locale]/(default)/webpages/contact/[id]/page.tsx new file mode 100644 index 000000000..b4685ff7a --- /dev/null +++ b/apps/core/app/[locale]/(default)/webpages/contact/[id]/page.tsx @@ -0,0 +1,92 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages } from 'next-intl/server'; +import { cache } from 'react'; + +import { client } from '~/client'; +import { graphql } from '~/client/graphql'; +import { revalidate } from '~/client/revalidate-target'; + +import { ContactUs } from './contact-us'; +import { ContactUsFragment } from './contact-us/fragment'; + +interface Props { + params: { id: string; locale: string }; +} + +const WebPageQuery = graphql( + ` + query WebPage($id: ID!) { + ...ContactUsFragment + node(id: $id) { + __typename + ... on ContactPage { + name + htmlBody + seo { + pageTitle + metaKeywords + metaDescription + } + } + } + } + `, + [ContactUsFragment], +); + +const getWebpageData = cache(async (variables: { id: string }) => { + const { data } = await client.fetch({ + document: WebPageQuery, + variables, + fetchOptions: { next: { revalidate } }, + }); + + return data; +}); + +export async function generateMetadata({ params }: Props): Promise { + const data = await getWebpageData({ id: decodeURIComponent(params.id) }); + const webpage = data.node?.__typename === 'ContactPage' ? data.node : null; + + if (!webpage) { + notFound(); + } + + const { seo } = webpage; + + return { + title: seo.pageTitle, + description: seo.metaDescription, + keywords: seo.metaKeywords, + }; +} + +export default async function WebPage({ params: { locale, id } }: Props) { + const data = await getWebpageData({ id: decodeURIComponent(id) }); + const webpage = data.node?.__typename === 'ContactPage' ? data.node : null; + + if (!webpage) { + notFound(); + } + + const messages = await getMessages({ locale }); + + const { name, htmlBody } = webpage; + + return ( + <> +
+

{name}

+
+
+ + + + + + ); +} + +export const runtime = 'edge'; diff --git a/apps/core/app/[locale]/(default)/webpages/normal/[id]/page.tsx b/apps/core/app/[locale]/(default)/webpages/normal/[id]/page.tsx new file mode 100644 index 000000000..921c61e76 --- /dev/null +++ b/apps/core/app/[locale]/(default)/webpages/normal/[id]/page.tsx @@ -0,0 +1,78 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { cache } from 'react'; + +import { client } from '~/client'; +import { graphql } from '~/client/graphql'; +import { revalidate } from '~/client/revalidate-target'; + +interface Props { + params: { id: string }; +} + +const NormalPageQuery = graphql(` + query NormalPage($id: ID!) { + node(id: $id) { + ... on NormalPage { + __typename + name + htmlBody + entityId + seo { + pageTitle + metaDescription + metaKeywords + } + } + } + } +`); + +const getWebpageData = cache(async (variables: { id: string }) => { + const { data } = await client.fetch({ + document: NormalPageQuery, + variables, + fetchOptions: { next: { revalidate } }, + }); + + if (data.node?.__typename !== 'NormalPage') { + return null; + } + + return data.node; +}); + +export async function generateMetadata({ params: { id } }: Props): Promise { + const webpage = await getWebpageData({ id }); + + if (!webpage) { + notFound(); + } + + const { seo } = webpage; + + return { + title: seo.pageTitle, + description: seo.metaDescription, + keywords: seo.metaKeywords, + }; +} + +export default async function WebPage({ params: { id } }: Props) { + const webpage = await getWebpageData({ id }); + + if (!webpage) { + notFound(); + } + + const { name, htmlBody } = webpage; + + return ( +
+

{name}

+
+
+ ); +} + +export const runtime = 'edge'; diff --git a/apps/core/client/queries/get-route.ts b/apps/core/client/queries/get-route.ts index a9cfe326d..a7fc3a556 100644 --- a/apps/core/client/queries/get-route.ts +++ b/apps/core/client/queries/get-route.ts @@ -15,6 +15,7 @@ const GET_ROUTE_QUERY = graphql(` } node { __typename + id ... on Product { entityId } @@ -24,9 +25,6 @@ const GET_ROUTE_QUERY = graphql(` ... on Brand { entityId } - ... on RawHtmlPage { - id - } } } } diff --git a/apps/core/client/queries/get-web-page.ts b/apps/core/client/queries/get-web-page.ts deleted file mode 100644 index 0fa2e2816..000000000 --- a/apps/core/client/queries/get-web-page.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { cache } from 'react'; - -import { client } from '..'; -import { graphql } from '../graphql'; -import { revalidate } from '../revalidate-target'; - -const WEB_PAGE_FRAGMENT = graphql(` - fragment WebPage on WebPage { - __typename - entityId - name - seo { - pageTitle - metaKeywords - metaDescription - } - } -`); - -const GET_WEB_PAGE_QUERY = graphql( - ` - query getWebPage($path: String!) { - site { - route(path: $path) { - node { - ...WebPage - ... on ContactPage { - contactFields - htmlBody - } - ... on NormalPage { - htmlBody - } - } - } - } - } - `, - [WEB_PAGE_FRAGMENT], -); - -export interface Options { - path: string; -} - -export const getWebPage = cache(async ({ path }: Options) => { - const response = await client.fetch({ - document: GET_WEB_PAGE_QUERY, - variables: { path }, - fetchOptions: { next: { revalidate } }, - }); - - const webpage = response.data.site.route.node; - - if (!webpage) { - return undefined; - } - - switch (webpage.__typename) { - case 'ContactPage': - case 'NormalPage': - return webpage; - - default: - return undefined; - } -}); diff --git a/apps/core/client/queries/get-web-pages.ts b/apps/core/client/queries/get-web-pages.ts deleted file mode 100644 index 34f711de9..000000000 --- a/apps/core/client/queries/get-web-pages.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; -import { cache } from 'react'; - -import { client } from '..'; -import { graphql } from '../graphql'; -import { revalidate } from '../revalidate-target'; -import { ExistingResultType } from '../util'; - -export type AvailableWebPages = ExistingResultType; - -const GET_WEB_PAGES_QUERY = graphql(` - query getWebPages { - site { - content { - pages(filters: { isVisibleInNavigation: true }) { - edges { - node { - __typename - name - ... on RawHtmlPage { - path - } - ... on ContactPage { - path - } - ... on NormalPage { - path - } - ... on BlogIndexPage { - path - } - ... on ExternalLinkPage { - link - } - } - } - } - } - } - } -`); - -export const getWebPages = cache(async () => { - const response = await client.fetch({ - document: GET_WEB_PAGES_QUERY, - fetchOptions: { next: { revalidate } }, - }); - - const { pages } = response.data.site.content; - - if (!pages.edges?.length) { - return []; - } - - return removeEdgesAndNodes(pages); -}); diff --git a/apps/core/components/forms/index.ts b/apps/core/components/forms/index.ts deleted file mode 100644 index 155a8cd9c..000000000 --- a/apps/core/components/forms/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -'use client'; - -export * from './contact-us'; diff --git a/apps/core/middlewares/with-routes.ts b/apps/core/middlewares/with-routes.ts index 0dbbea346..375cebb7a 100644 --- a/apps/core/middlewares/with-routes.ts +++ b/apps/core/middlewares/with-routes.ts @@ -50,6 +50,8 @@ const NodeSchema = z.union([ z.object({ __typename: z.literal('Product'), entityId: z.number() }), z.object({ __typename: z.literal('Category'), entityId: z.number() }), z.object({ __typename: z.literal('Brand'), entityId: z.number() }), + z.object({ __typename: z.literal('ContactPage'), id: z.string() }), + z.object({ __typename: z.literal('NormalPage'), id: z.string() }), z.object({ __typename: z.literal('RawHtmlPage'), id: z.string() }), ]); @@ -230,6 +232,16 @@ export const withRoutes: MiddlewareFactory = () => { break; } + case 'NormalPage': { + url = `/${locale}/webpages/normal/${node.id}`; + break; + } + + case 'ContactPage': { + url = `/${locale}/webpages/contact/${node.id}`; + break; + } + case 'RawHtmlPage': { const { htmlBody } = await getRawWebPageContent(node.id);