Skip to content

Commit

Permalink
feat: Integrate with Butter CMS (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
soniaklimas authored and karolkarolka committed Dec 4, 2024
1 parent 24c56de commit 8c8dec3
Show file tree
Hide file tree
Showing 44 changed files with 585 additions and 285 deletions.
1 change: 1 addition & 0 deletions apps/storefront/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const nextConfig = withNextIntl({
hostname: "*.saleor.cloud",
},
],
domains: ["cdn.buttercms.com"],
},
reactStrictMode: true,
transpilePackages: ["@nimara/ui"],
Expand Down
53 changes: 12 additions & 41 deletions apps/storefront/src/app/[locale]/(main)/_components/hero-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,42 @@
import { ArrowRight } from "lucide-react";

import type { Attribute } from "@nimara/domain/objects/Attribute";
import type { SearchContext } from "@nimara/infrastructure/use-cases/search/types";
import type { PageField } from "@nimara/domain/objects/CMSPage";
import { Button } from "@nimara/ui/components/button";

import { Link } from "@/i18n/routing";
import { getAttributes } from "@/lib/helpers";
import { createFieldsMap, type FieldsMap } from "@/lib/cms";
import { paths } from "@/lib/paths";
import { getCurrentRegion } from "@/regions/server";
import { searchService } from "@/services/search";

const attributeSlugs = [
"homepage-banner-header",
"homepage-banner-image",
"homepage-banner-button-text",
];

export const HeroBanner = async ({
attributes,
fields,
}: {
attributes: Attribute[] | undefined;
fields: PageField[] | undefined;
}) => {
const region = await getCurrentRegion();

const searchContext = {
currency: region.market.currency,
channel: region.market.channel,
languageCode: region.language.code,
} satisfies SearchContext;

if (attributes?.length === 0) {
if (!fields || fields.length === 0) {
return null;
}

const attributesMap = getAttributes(attributes, attributeSlugs);
const fieldsMap: FieldsMap = createFieldsMap(fields);

const header = attributesMap["homepage-banner-header"];
const buttonText = attributesMap["homepage-banner-button-text"];
const image = attributesMap["homepage-banner-image"];

const productId = image?.values[0]?.reference as string;

const { results: products } = await searchService.search(
{
productIds: [productId],
limit: 1,
},
searchContext,
);
const header = fieldsMap["homepage-banner-header"]?.text;
const buttonText = fieldsMap["homepage-banner-button-text"]?.text;
const image = fieldsMap["homepage-banner-image-test"]?.imageUrl;

return (
<div className="mb-14 flex flex-col items-center bg-stone-100 sm:h-[27rem] sm:flex-row">
<div className="order-last p-8 sm:order-first sm:basis-1/2 lg:p-16">
<h2 className="pb-8 text-3xl font-medium lg:text-5xl">
{header?.values[0]?.plainText}
</h2>
<h2 className="pb-8 text-3xl font-medium lg:text-5xl">{header}</h2>
<Button asChild>
<Link href={paths.search.asPath()}>
{buttonText?.values[0]?.plainText}
{buttonText}
<ArrowRight className="h-4 w-5 pl-1" />
</Link>
</Button>
</div>
<div className="sm-order-last order-first w-full sm:basis-1/2">
<div
className="h-[22rem] bg-cover bg-center sm:h-[27rem]"
style={{ backgroundImage: `url(${products[0]?.thumbnail?.url})` }}
style={{ backgroundImage: `url(${image})` }}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ArrowRight } from "lucide-react";
import { getTranslations } from "next-intl/server";

import type { Attribute } from "@nimara/domain/objects/Attribute";
import type { PageField } from "@nimara/domain/objects/CMSPage";
import type { SearchContext } from "@nimara/infrastructure/use-cases/search/types";
import { Button } from "@nimara/ui/components/button";
import {
Expand All @@ -12,25 +12,15 @@ import {

import { SearchProductCard } from "@/components/search-product-card";
import { Link } from "@/i18n/routing";
import { getAttributes } from "@/lib/helpers";
import { createFieldsMap, type FieldsMap } from "@/lib/cms";
import { paths } from "@/lib/paths";
import { getCurrentRegion } from "@/regions/server";
import { searchService } from "@/services/search";

const attributeSlugs = [
"homepage-grid-item-header",
"homepage-grid-item-subheader",
"homepage-grid-item-image",
"homepage-button-text",
"homepage-grid-item-header-font-color",
"homepage-grid-item-subheader-font-color",
"carousel-products",
];

export const ProductsGrid = async ({
attributes,
fields,
}: {
attributes: Attribute[] | undefined;
fields: PageField[] | undefined;
}) => {
const [region, t] = await Promise.all([
getCurrentRegion(),
Expand All @@ -43,37 +33,27 @@ export const ProductsGrid = async ({
languageCode: region.language.code,
} satisfies SearchContext;

if (attributes?.length === 0) {
if (!fields || fields.length === 0) {
return null;
}

const attributesMap = getAttributes(attributes, attributeSlugs);
const fieldsMap: FieldsMap = createFieldsMap(fields);

const header = attributesMap["homepage-grid-item-header"];
const subheader = attributesMap["homepage-grid-item-subheader"];
const image = attributesMap["homepage-grid-item-image"];
const buttonText = attributesMap["homepage-button-text"];
const headerFontColor = attributesMap["homepage-grid-item-header-font-color"];
const header = fieldsMap["homepage-grid-item-header"]?.text;
const subheader = fieldsMap["homepage-grid-item-subheader"]?.text;
const image = fieldsMap["homepage-grid-item-image-test"]?.imageUrl;
const buttonText = fieldsMap["homepage-button-text"]?.text;
const headerFontColor =
fieldsMap["homepage-grid-item-header-font-color"]?.text;
const subheaderFontColor =
attributesMap["homepage-grid-item-subheader-font-color"];
const gridProducts = attributesMap["carousel-products"];

const imageProductId = image?.values[0]?.reference as string;
const gridProductsIds = gridProducts?.values?.map(
(product) => product?.reference,
) as string[];
fieldsMap["homepage-grid-item-subheader-font-color"]?.text;
const gridProducts = fieldsMap["carousel-products"];

const { results: gridImageProduct } = await searchService.search(
{
productIds: imageProductId ? [imageProductId] : [],
limit: 1,
},
searchContext,
);
const gridProductsIds = gridProducts?.reference;

const { results: products } = await searchService.search(
{
productIds: gridProductsIds.length ? [...gridProductsIds] : [],
productIds: gridProductsIds?.length ? [...gridProductsIds] : [],
limit: 7,
},
searchContext,
Expand All @@ -85,24 +65,24 @@ export const ProductsGrid = async ({
<div
className="relative min-h-44 border-stone-200 bg-cover bg-center p-6"
style={{
backgroundImage: `url(${gridImageProduct[0]?.thumbnail?.url})`,
backgroundImage: `url(${image})`,
}}
>
<h2
className="text-2xl opacity-100"
style={{
color: `${headerFontColor?.values[0]?.value ?? "#44403c"}`,
color: `${headerFontColor ?? "#44403c"}`,
}}
>
{header?.values[0]?.plainText}
{header}
</h2>
<h3
className="text-sm"
style={{
color: `${subheaderFontColor?.values[0]?.value ?? "#78716c"}`,
color: `${subheaderFontColor ?? "#78716c"}`,
}}
>
{subheader?.values[0]?.plainText}
{subheader}
</h3>
<Button
className="absolute bottom-4 right-4 p-3"
Expand Down Expand Up @@ -144,7 +124,7 @@ export const ProductsGrid = async ({
<div className="mx-auto mb-14">
<Button variant="outline" asChild>
<Link href={paths.search.asPath()}>
{buttonText?.values[0]?.plainText}
{buttonText}
<ArrowRight className="h-4 w-5 pl-1" />
</Link>
</Button>
Expand Down
2 changes: 1 addition & 1 deletion apps/storefront/src/app/[locale]/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type ReactNode } from "react";
import { Footer } from "@/components/footer";
import { Header } from "@/components/header";
import { getCurrentRegion } from "@/regions/server";
import { cmsMenuService } from "@/services";
import { cmsMenuService } from "@/services/cms";

import { Navigation } from "./_components/navigation";

Expand Down
14 changes: 9 additions & 5 deletions apps/storefront/src/app/[locale]/(main)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { type Metadata } from "next";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";

import { PageType } from "@nimara/domain/objects/CMSPage";

import { getAccessToken } from "@/auth";
import { CACHE_TTL } from "@/config";
import { JsonLd, websiteToJsonLd } from "@/lib/json-ld";
import { getCurrentRegion } from "@/regions/server";
import { cmsPageService, userService } from "@/services";
import { userService } from "@/services";
import { cmsPageService } from "@/services/cms";

import { AccountNotifications } from "./_components/account-notifications";
import { HeroBanner } from "./_components/hero-banner";
Expand Down Expand Up @@ -42,8 +45,9 @@ export default async function Page() {
]);

const page = await cmsPageService.cmsPageGet({
languageCode: region.language.code,
pageType: PageType.HOMEPAGE,
slug: "home",
languageCode: region.language.code,
options: {
next: {
tags: ["CMS:home"],
Expand All @@ -54,8 +58,8 @@ export default async function Page() {

return (
<section className="grid w-full content-start">
<HeroBanner attributes={page?.attributes} />
<ProductsGrid attributes={page?.attributes} />
<HeroBanner fields={page?.fields} />
<ProductsGrid fields={page?.fields} />
<div>
<AccountNotifications user={user} />
</div>
Expand Down
9 changes: 6 additions & 3 deletions apps/storefront/src/app/[locale]/(main)/page/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { type Metadata } from "next";
import type { Metadata } from "next";
import { notFound } from "next/navigation";

import { PageType } from "@nimara/domain/objects/CMSPage";

import { StaticPage } from "@/components/static-page";
import { CACHE_TTL } from "@/config";
import { getCurrentRegion } from "@/regions/server";
import { cmsPageService } from "@/services";
import { cmsPageService } from "@/services/cms";

export async function generateMetadata(props: {
params: Promise<{ slug: string }>;
Expand Down Expand Up @@ -41,8 +43,9 @@ export default async function Page(props: {
const region = await getCurrentRegion();

const page = await cmsPageService.cmsPageGet({
languageCode: region.language.code,
pageType: PageType.STATIC_PAGE,
slug,
languageCode: region.language.code,
options: {
next: {
tags: [`CMS:${slug}`],
Expand Down
2 changes: 1 addition & 1 deletion apps/storefront/src/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Link } from "@/i18n/routing";
import { generateLinkUrl } from "@/lib/helpers";
import { paths } from "@/lib/paths";
import { getCurrentRegion } from "@/regions/server";
import { cmsMenuService } from "@/services";
import { cmsMenuService } from "@/services/cms";

export const Footer = async () => {
const [region, t] = await Promise.all([
Expand Down
3 changes: 2 additions & 1 deletion apps/storefront/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { clientEnvs } from "@/envs/client";
import { Link } from "@/i18n/routing";
import { paths } from "@/lib/paths";
import { getCurrentRegion } from "@/regions/server";
import { cartService, cmsMenuService, userService } from "@/services";
import { cartService, userService } from "@/services";
import { cmsMenuService } from "@/services/cms";

import { Logo } from "./logo";
import { MobileSearch } from "./mobile-search";
Expand Down
24 changes: 21 additions & 3 deletions apps/storefront/src/components/static-page/static-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,33 @@ import edjsHTML from "editorjs-html";
import React from "react";
import xss from "xss";

import type { Maybe } from "@/lib/types";

const parser = edjsHTML();

export const StaticPage = async ({ body }: { body: string | null }) => {
const contentHtml = body ? parser.parse(JSON.parse(body)) : null;
type EditorJSContent = {
blocks?: Array<string>;
};

export const StaticPage = async ({ body }: { body: Maybe<string> }) => {
let contentHtml: string[] | null = null;

if (body) {
try {
const parsedContent = JSON.parse(body) as EditorJSContent;

if (parsedContent && parsedContent.blocks) {
contentHtml = parser.parse(parsedContent);
}
} catch (error) {
contentHtml = [body];
}
}

return (
<div className="container">
{contentHtml ? (
<div className="prose min-w-full prose-h1:my-4 prose-h1:text-center prose-h1:text-4xl">
<div className="prose min-w-full prose-h1:my-4 prose-h1:text-center prose-h1:text-4xl prose-h2:text-center prose-h2:text-4xl">
{contentHtml.map((content) => (
<div
key={content}
Expand Down
4 changes: 4 additions & 0 deletions apps/storefront/src/envs/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const schema = z.object({
.default("LOCAL"),

PAYMENT_APP_ID: z.string().default("dev.marina-stripe-saleor-dev"),

NEXT_PUBLIC_BUTTER_CMS_API_KEY: z.string(),
});

export const clientEnvs = schema.parse({
Expand All @@ -30,4 +32,6 @@ export const clientEnvs = schema.parse({
ENVIRONMENT: process.env.NEXT_PUBLIC_ENVIRONMENT,

PAYMENT_APP_ID: process.env.NEXT_PUBLIC_PAYMENT_APP_ID,

NEXT_PUBLIC_BUTTER_CMS_API_KEY: process.env.NEXT_PUBLIC_BUTTER_CMS_API_KEY,
});
22 changes: 22 additions & 0 deletions apps/storefront/src/lib/cms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { PageField } from "@nimara/domain/objects/CMSPage";

export type FieldsMap = {
[key: string]: {
imageUrl?: string;
reference?: string[];
text?: string;
};
};

export const createFieldsMap = (fields: PageField[]): FieldsMap => {
return Object.fromEntries(
fields.map((field) => [
field.slug,
{
text: field.text,
imageUrl: field.imageUrl,
reference: field.reference,
},
]),
);
};
Loading

0 comments on commit 8c8dec3

Please sign in to comment.