diff --git a/app/consulting/[filename]/consulting.tsx b/app/consulting/[filename]/consulting.tsx new file mode 100644 index 0000000000..ce4dc1574b --- /dev/null +++ b/app/consulting/[filename]/consulting.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { BookingButton, BuiltOnAzure, ClientLogos } from "@/components/blocks"; +import { Blocks } from "@/components/blocks-renderer"; +import { Booking } from "@/components/blocks/booking"; +import { componentRenderer } from "@/components/blocks/mdxComponentRenderer"; +import { CallToAction } from "@/components/callToAction/callToAction"; +import MediaCards from "@/components/consulting/mediaCard/mediaCards"; +import { Marketing } from "@/components/marketing/Marketing"; +import TechnologyCards from "@/components/technologyCard/technologyCards"; +import { TestimonialRow } from "@/components/testimonials/TestimonialRow"; +import { Benefits } from "@/components/util/consulting/benefits"; +import { Container } from "@/components/util/container"; +import { Section } from "@/components/util/section"; +import { sanitiseXSS, spanWhitelist } from "@/helpers/validator"; +import { removeExtension } from "@/services/client/utils.service"; +import { Breadcrumbs } from "app/components/breadcrumb"; + +import { ReactElement } from "react"; +import ReactDOMServer from "react-dom/server"; +import { tinaField } from "tinacms/dist/react"; +import { TinaMarkdown } from "tinacms/dist/rich-text"; + +export default function Consulting({ tinaProps, props }) { + const { techCards, marketingData, categories, mediaCardProps } = props; + const { data } = tinaProps; + return ( + <> +
+ +
+
+ + + +
+
+ +
+
+ + +
+
+
+
+ + {data.consulting.afterBody ? ( +
+ +
+ ) : ( + <> + )} + + +
+
+ +
+ +

Companies we have worked with

+ +
+
+ {!!techCards.length && ( +
+ + + +
+ )} + {!!mediaCardProps.length && ( +
+ + + +
+ )} + {data?.consulting?.callToAction?.showCallToAction && ( + +

+
+ )} + +
+ +
+ + ); +} + +const parseCallToAction = ( + content: string, + project: string, + data: { project?: string } +) => { + const HTMLelement: ReactElement = ( + + {project} + + ); + + return sanitiseXSS( + content?.replace("{{TITLE}}", ReactDOMServer.renderToString(HTMLelement)), + spanWhitelist + ); +}; diff --git a/app/consulting/[filename]/page.tsx b/app/consulting/[filename]/page.tsx new file mode 100644 index 0000000000..58e6d78369 --- /dev/null +++ b/app/consulting/[filename]/page.tsx @@ -0,0 +1,152 @@ +import { MediaCardProps } from "@/components/consulting/mediaCard/mediaCard"; +import { getRandomTestimonialsByCategory } from "@/helpers/getTestimonials"; +import client from "@/tina/client"; +import "aos/dist/aos.css"; // This is important to keep the animation +import { TODAY } from "hooks/useFetchEvents"; +import { useSEO } from "hooks/useSeo"; +import { Metadata } from "next"; +import { Open_Sans } from "next/font/google"; +import { TinaClient } from "../../tina-client"; +import ConsultingPage from "./consulting"; + +const openSans = Open_Sans({ + variable: "--open-sans-font", + subsets: ["latin"], + display: "swap", + weight: ["300", "400", "600"], +}); + +export const dynamicParams = false; + +export async function generateStaticParams() { + let pageListData = await client.queries.consultingConnection(); + const allPagesListData = pageListData; + + while (pageListData.data.consultingConnection.pageInfo.hasNextPage) { + const lastCursor = + pageListData.data.consultingConnection.pageInfo.endCursor; + pageListData = await client.queries.consultingConnection({ + after: lastCursor, + }); + + allPagesListData.data.consultingConnection.edges.push( + ...pageListData.data.consultingConnection.edges + ); + } + + const pages = allPagesListData.data.consultingConnection.edges.map( + (page) => ({ + filename: page.node._sys.filename, + }) + ); + + return pages; +} + +const getData = async (filename: string) => { + const tinaProps = await client.queries.consultingContentQuery({ + relativePath: `${filename}.mdx`, + date: TODAY.toISOString(), + }); + + const seo = tinaProps.data.consulting.seo; + + const categories = + tinaProps.data.consulting?.testimonialCategories + ?.map((category) => category?.testimonialCategory?.name) + ?.filter((item) => !!item) || []; + + const testimonialsResult = await getRandomTestimonialsByCategory(categories); + + const technologyCardNames = + tinaProps.data.consulting.technologies?.technologyCards?.reduce( + (pre, cur) => { + !!cur.technologyCard?.name && pre.push(cur.technologyCard.name); + return pre; + }, + [] + ) || []; + const technologyCardsProps = await client.queries.technologyCardContentQuery({ + cardNames: technologyCardNames, + }); + + const technologyCardDocs = + technologyCardsProps?.data.technologiesConnection.edges.map((n) => n.node); + const techCards = + tinaProps.data.consulting.technologies?.technologyCards?.map((c) => ({ + ...technologyCardDocs?.find( + (n) => !!n.name && n.name === c.technologyCard?.name + ), + })) || []; + + const categoriesData = + tinaProps.data.consulting.testimonialCategories + ?.filter((category) => !!category?.testimonialCategory) + ?.map((category) => category.testimonialCategory.name) ?? []; + + const mediaCardProps = + tinaProps.data.consulting?.medias?.mediaCards?.map( + (m): MediaCardProps => ({ + type: m.type as MediaCardProps["type"], + content: m.content, + }) + ) || []; + const marketingSection = await client.queries.marketing({ + relativePath: "/why-choose-ssw.mdx", + }); + + return { + props: { + data: tinaProps.data, + query: tinaProps.query, + variables: tinaProps.variables, + testimonialsResult, + techCards: techCards, + marketingData: marketingSection.data, + categories: categoriesData, + mediaCardProps: mediaCardProps, + header: { + url: tinaProps.data.global.header.url, + }, + seo, + ...tinaProps, + }, + }; +}; + +type GenerateMetaDataProps = { + params: { filename: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}; + +export async function generateMetadata({ + params, +}: GenerateMetaDataProps): Promise { + const tinaProps = await getData(params.filename); + + const seo = tinaProps.props.seo; + if (seo && !seo.canonical) { + seo.canonical = `${tinaProps.props.header.url}consulting/${params.filename}`; + } + + // eslint-disable-next-line react-hooks/rules-of-hooks + const { seoProps } = useSEO(seo); + + return { ...seoProps }; +} + +export default async function Consulting({ + params, +}: { + params: { filename: string }; +}) { + const { filename } = params; + + const { props } = await getData(filename); + + return ( +
+ +
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 56194a4c81..7286932aaa 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -96,6 +96,5 @@ export default async function HomePage({ }) { const { filename } = params; const tinaProps = await getData(filename); - const buildTime = new Date().toLocaleString(); - return ; + return ; } diff --git a/components/blocks/booking.tsx b/components/blocks/booking.tsx index 9c73438f24..7f598ffc17 100644 --- a/components/blocks/booking.tsx +++ b/components/blocks/booking.tsx @@ -4,6 +4,7 @@ import { tinaField } from "tinacms/dist/react"; import { sanitiseXSS, spanWhitelist } from "../../helpers/validator"; import { CustomLink } from "../customLink"; import { Container } from "../util/container"; +import VideoBackground from "./videoBackground"; export const Booking: FC<{ title?: string; @@ -33,20 +34,11 @@ export const Booking: FC<{ - + ); }; diff --git a/components/blocks/mdxComponentRenderer.tsx b/components/blocks/mdxComponentRenderer.tsx index d424a5ff72..8b6281783b 100644 --- a/components/blocks/mdxComponentRenderer.tsx +++ b/components/blocks/mdxComponentRenderer.tsx @@ -115,9 +115,11 @@ const VerticalImageLayout = dynamic(() => const VerticalListItem = dynamic(() => import("./verticalListItem").then((mod) => mod.VerticalListItem) ); -const VideoEmbed = dynamic(() => - import("./videoEmbed").then((mod) => mod.VideoEmbed) +const VideoEmbed = dynamic( + () => import("./videoEmbed").then((mod) => mod.VideoEmbed), + { ssr: false } ); + const YoutubePlaylistBlock = dynamic(() => import("./youtubePlaylist").then((mod) => mod.YoutubePlaylistBlock) ); diff --git a/components/blocks/videoBackground.tsx b/components/blocks/videoBackground.tsx new file mode 100644 index 0000000000..39563ea981 --- /dev/null +++ b/components/blocks/videoBackground.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +interface VideoBackgroundProps { + videoBackground: string; + tinaField: (props: unknown, field: string) => string; + props: unknown; +} + +const VideoBackground: React.FC = ({ + videoBackground, + tinaField, + props, +}) => { + return ( + + ); +}; + +export default VideoBackground; diff --git a/components/util/consulting/benefits.tsx b/components/util/consulting/benefits.tsx index 088e239007..37568dc17d 100644 --- a/components/util/consulting/benefits.tsx +++ b/components/util/consulting/benefits.tsx @@ -1,9 +1,11 @@ import classNames from "classnames"; -import Image from "next/image"; +import dynamic from "next/dynamic"; import { tinaField } from "tinacms/dist/react"; import { TinaMarkdown } from "tinacms/dist/rich-text"; import { CustomLink } from "../../customLink"; +const Image = dynamic(() => import("next/image"), { ssr: true }); + const BenefitCard = (props) => { const { image, title, description, linkURL, linkName } = props.data; return ( @@ -25,6 +27,7 @@ const BenefitCard = (props) => { width={120} height={120} alt={title || "benefit icon"} + loading="lazy" /> )} diff --git a/pages/consulting/[filename].tsx b/pages/consulting/[filename].tsx deleted file mode 100644 index 928bf4929c..0000000000 --- a/pages/consulting/[filename].tsx +++ /dev/null @@ -1,274 +0,0 @@ -import { CallToAction } from "@/components/callToAction/callToAction"; -import { client } from "@/tina/client"; -import { TODAY } from "hooks/useFetchEvents"; -import { InferGetStaticPropsType } from "next"; -import { ReactElement } from "react"; -import ReactDOMServer from "react-dom/server"; -import { tinaField, useTina } from "tinacms/dist/react"; -import { TinaMarkdown } from "tinacms/dist/rich-text"; -import { BuiltOnAzure, ClientLogos } from "../../components/blocks"; -import { Blocks } from "../../components/blocks-renderer"; -import { Booking } from "../../components/blocks/booking"; -import { Breadcrumbs } from "../../components/blocks/breadcrumbs"; -import { componentRenderer } from "../../components/blocks/mdxComponentRenderer"; -import { BookingButton } from "../../components/bookingButton/bookingButton"; -import { MediaCardProps } from "../../components/consulting/mediaCard/mediaCard"; -import MediaCards from "../../components/consulting/mediaCard/mediaCards"; -import { Layout } from "../../components/layout"; -import { Marketing } from "../../components/marketing/Marketing"; -import TechnologyCards from "../../components/technologyCard/technologyCards"; -import { TestimonialRow } from "../../components/testimonials/TestimonialRow"; -import { Benefits } from "../../components/util/consulting/benefits"; -import { Container } from "../../components/util/container"; -import { Section } from "../../components/util/section"; -import { SEO } from "../../components/util/seo"; -import { RecaptchaContext } from "../../context/RecaptchaContext"; -import { getRandomTestimonialsByCategory } from "../../helpers/getTestimonials"; -import { sanitiseXSS, spanWhitelist } from "../../helpers/validator"; -import { removeExtension } from "../../services/client/utils.service"; - -export default function ConsultingPage( - props: InferGetStaticPropsType -) { - const { data } = useTina({ - data: props.data, - query: props.query, - variables: props.variables, - }); - const technologyCardDocs = - props.technologyCards.data.technologiesConnection.edges.map((n) => n.node); - const techCards = - data.consulting.technologies?.technologyCards?.map((c) => ({ - ...technologyCardDocs.find( - (n) => !!n.name && n.name === c.technologyCard?.name - ), - })) || []; - - const mediaCardProps = - data.consulting.medias?.mediaCards?.map((m) => ({ - type: m.type as MediaCardProps["type"], - content: m.content, - })) || []; - - const categories = - data.consulting.testimonialCategories - ?.filter((category) => !!category?.testimonialCategory) - ?.map((category) => category.testimonialCategory.name) ?? []; - - return ( - - - -
- -
-
- - - -
-
- -
-
- - -
-
-
-
- - {data.consulting.afterBody ? ( -
- -
- ) : ( - <> - )} - - -
-
- -
- -

Companies we have worked with

- -
-
- {!!techCards.length && ( -
- - - -
- )} - {!!mediaCardProps.length && ( -
- - - -
- )} - {data?.consulting?.callToAction?.showCallToAction && ( - -

-
- )} - -
- -
- - - ); -} - -const parseCallToAction = ( - content: string, - project: string, - data: { project?: string } -) => { - const HTMLelement: ReactElement = ( - - {project} - - ); - - return sanitiseXSS( - content?.replace("{{TITLE}}", ReactDOMServer.renderToString(HTMLelement)), - spanWhitelist - ); -}; - -export const getStaticProps = async ({ params }) => { - const tinaProps = await client.queries.consultingContentQuery({ - relativePath: `${params.filename}.mdx`, - date: TODAY.toISOString(), - }); - - const categories = - tinaProps.data.consulting?.testimonialCategories - ?.map((category) => category?.testimonialCategory?.name) - ?.filter((item) => !!item) || []; - - const testimonialsResult = await getRandomTestimonialsByCategory(categories); - - const seo = tinaProps.data.consulting.seo; - if (seo && !seo.canonical) { - seo.canonical = `${tinaProps.data.global.header.url}consulting/${params.filename}`; - } - - const technologyCardNames = - tinaProps.data.consulting.technologies?.technologyCards?.reduce( - (pre, cur) => { - !!cur.technologyCard?.name && pre.push(cur.technologyCard.name); - return pre; - }, - [] - ) || []; - const technologyCardsProps = await client.queries.technologyCardContentQuery({ - cardNames: technologyCardNames, - }); - - const marketingSection = await client.queries.marketing({ - relativePath: "/why-choose-ssw.mdx", - }); - - return { - props: { - data: tinaProps.data, - query: tinaProps.query, - variables: tinaProps.variables, - testimonialsResult, - technologyCards: technologyCardsProps, - marketingData: marketingSection.data, - env: { - GOOGLE_RECAPTCHA_SITE_KEY: - process.env.GOOGLE_RECAPTCHA_SITE_KEY || null, - }, - seo, - }, - }; -}; - -export const getStaticPaths = async () => { - let pageListData = await client.queries.consultingConnection(); - const allPagesListData = pageListData; - - while (pageListData.data.consultingConnection.pageInfo.hasNextPage) { - const lastCursor = - pageListData.data.consultingConnection.pageInfo.endCursor; - pageListData = await client.queries.consultingConnection({ - after: lastCursor, - }); - - allPagesListData.data.consultingConnection.edges.push( - ...pageListData.data.consultingConnection.edges - ); - } - - return { - paths: allPagesListData.data.consultingConnection.edges.map((page) => ({ - params: { filename: page.node._sys.filename }, - })), - fallback: false, - }; -};