Skip to content

Commit

Permalink
feat: PDP and search page improvements (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
soniaklimas authored Dec 19, 2024
1 parent 0a96ffa commit 80eee2a
Show file tree
Hide file tree
Showing 32 changed files with 1,871 additions and 1,098 deletions.
22 changes: 20 additions & 2 deletions apps/storefront/messages/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"artists": "Artists",
"in-stock": "In stock",
"is-exclusive": "Is exclusive",
"is-digital": "Is digital"
"is-digital": "Is digital",
"size": "Size",
"color": "Color"
},
"search": {
"go-to-product": "Go to product {name}",
Expand Down Expand Up @@ -110,7 +112,12 @@
"products": {
"variant-select-label": "Variant select label",
"variant-select": "Variant select",
"label-slug": "Label-{slug}"
"label-slug": "Label-{slug}",
"description": "Description",
"free-shipping": "Free shipping",
"standard-parcel": "Standard parcel",
"free-30-days": "Free 30 days return policy",
"you-may-also-like": "You may also like"
},
"address": {
"companyName": "Company name",
Expand Down Expand Up @@ -350,5 +357,16 @@
"faq": "FAQ",
"privacy-policy": "Privacy Policy",
"terms-of-use": "Terms of Use"
},
"colors": {
"yellow": "Yellow",
"black": "Black",
"white": "White",
"beige": "Beige",
"grey": "Grey",
"khaki": "Khaki",
"pink": "Pink",
"red": "Red",
"green": "Green"
}
}
3 changes: 2 additions & 1 deletion apps/storefront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@formatjs/intl-localematcher": "0.5.9",
"@hookform/resolvers": "3.6.0",
"@sentry/integrations": "^7.114.0",
"@sentry/nextjs": "^8.12.0",
"@sentry/nextjs": "^8.34.0",
"@sentry/types": "^8.12.0",
"@svgr/webpack": "8.1.0",
"@uidotdev/usehooks": "2.4.1",
Expand All @@ -32,6 +32,7 @@
"next-auth": "5.0.0-beta.17",
"next-intl": "3.23.5",
"nextjs-routes": "2.1.0",
"nuqs": "2.1.1",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-error-boundary": "4.0.13",
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 @@ -18,7 +18,7 @@ export default async function Layout({ children }: { children: ReactNode }) {

return (
<>
<div className="sticky top-0 isolate z-50 bg-background py-4">
<div className="sticky top-0 isolate z-50 bg-background py-4 md:pb-0">
<Header />
<Navigation menu={menu?.menu} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Attribute } from "@nimara/domain/objects/Attribute";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@nimara/ui/components/accordion";
import { RichText } from "@nimara/ui/components/rich-text";
import { parseEditorJSData } from "@nimara/ui/lib/richText";

export const AttributesDropdown = ({
attributes,
}: {
attributes: Attribute[];
}) => {
if (!attributes.length) {
return null;
}

return (
<Accordion className="mt-4" type="single" collapsible>
{attributes.map((attribute) => {
if (
!attribute.values.some(
(val) => val.richText && parseEditorJSData(val.richText),
)
) {
return;
}

return (
<AccordionItem key={attribute.slug} value={attribute.name}>
<AccordionTrigger className="capitalize">
{attribute.name}
</AccordionTrigger>
<AccordionContent>
{attribute.values.map((val) =>
val.richText ? (
<RichText key={val.name} jsonStringData={val.richText} />
) : (
<p key={val.name}>{val.name}</p>
),
)}
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"use client";

import { Truck, Undo2 } from "lucide-react";
import Image from "next/image";
import { useTranslations } from "next-intl";

import type { Cart } from "@nimara/domain/objects/Cart";
import type {
Product,
ProductAvailability,
} from "@nimara/domain/objects/Product";
import type { User } from "@nimara/domain/objects/User";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@nimara/ui/components/alert";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@nimara/ui/components/carousel";

import { ProductImagePlaceholder } from "@/components/product-image-placeholder";
import { SearchProductCard } from "@/components/search-product-card";

import { useVariantSelection } from "../hooks/useVariantSelection";
import { AttributesDropdown } from "./attributes-dropdown";
import { VariantSelector } from "./variant-selector";
import { getImagesToDisplay } from "./variant-selector-utils";

export const ProductDisplay = ({
product,
availability,
cart,
user,
}: {
availability: ProductAvailability;
cart: Cart | null;
product: Product;
user: (User & { accessToken: string | undefined }) | null;
}) => {
const t = useTranslations("products");

const { chosenVariant, looselyMatchingVariants } = useVariantSelection({
product,
productAvailability: availability,
cart,
});

const { images, name, description } = product;

const imagesToDisplay = getImagesToDisplay({
chosenVariant,
looselyMatchingVariants,
productImages: images,
});

const hasFreeShipping = !!product.attributes
.find(({ slug }) => slug === "free-shipping")
?.values.find(({ boolean }) => boolean);

const hasFreeReturn = !!product.attributes
.find(({ slug }) => slug === "free-return")
?.values.find(({ boolean }) => boolean);

const attributesToDisplay = product.attributes.filter(
({ slug }) => slug !== "free-shipping" && slug !== "free-return",
);

if (description && description?.length > 0) {
attributesToDisplay.unshift({
name: "description",
slug: "description",
type: "RICH_TEXT",
values: [
{
name: "description",
slug: "description",
richText: product.description ?? "",
boolean: false,
value: "",
date: undefined,
dateTime: undefined,
reference: undefined,
plainText: "",
},
],
});
}

return (
<div className="relative w-full">
<div className="my-6 grid gap-10 md:grid-cols-12 md:gap-4">
<div className="relative max-md:hidden md:col-span-6 [&>*]:pb-2">
{imagesToDisplay.length ? (
<>
{imagesToDisplay.map(({ url, alt }, i) => (
<Image
src={url}
key={url}
alt={alt || name}
height={500}
width={500}
priority={i === 0}
sizes="(max-width: 960px) 100vw, 50vw"
className="h-auto w-full"
/>
))}
</>
) : (
<ProductImagePlaceholder />
)}
</div>

<Carousel className="md:hidden">
<CarouselContent>
{imagesToDisplay.map(({ url, alt }) => (
<CarouselItem key={url}>
<Image
src={url}
alt={alt || name}
width={250}
height={250}
sizes="(max-width: 960px) 100vw, 1vw"
className="h-full w-full object-cover"
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>

<div className="md:col-span-5 md:col-start-8">
<section className="sticky top-28 px-1 pt-10">
<h1 className="text-2xl text-black">{name}</h1>
<VariantSelector
cart={cart}
product={product}
productAvailability={availability}
user={user}
/>

{hasFreeShipping && (
<Alert>
<Truck className="size-4" />
<AlertTitle>{t("free-shipping")}</AlertTitle>
<AlertDescription>{t("standard-parcel")}</AlertDescription>
</Alert>
)}

{hasFreeReturn && (
<Alert className="mt-2">
<Undo2 className="size-4" />
<AlertTitle>{t("free-30-days")}</AlertTitle>
</Alert>
)}

<AttributesDropdown attributes={attributesToDisplay} />
</section>
</div>
</div>

{product.relatedProducts.length > 0 && (
<div className="mb-7 mt-10 md:mb-14 md:mt-20">
<h2 className="mb-4 text-4xl text-black">{t("you-may-also-like")}</h2>
<Carousel>
<CarouselContent>
{product.relatedProducts.map((product) => (
<CarouselItem
key={product.id}
className="w-1/1 h-full flex-none md:w-1/5"
>
<SearchProductCard
product={product}
sizes="(max-width: 360px) 195px, (max-width: 720px) 379px, 1vw"
height={200}
width={200}
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
)}
</div>
);
};
Loading

0 comments on commit 80eee2a

Please sign in to comment.