Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Integrate with Butter CMS #66

Merged
merged 8 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/storefront/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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({
params: { slug },
Expand Down Expand Up @@ -37,8 +39,9 @@ export default async function Page({
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