mirror of
https://github.com/vercel/commerce.git
synced 2025-05-11 04:07:50 +00:00
add PDP content
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
fab2a5e967
commit
cc2c79764d
@ -5,6 +5,7 @@ import { Suspense } from 'react';
|
||||
import BreadcrumbComponent from 'components/breadcrumb';
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import Footer from 'components/layout/footer';
|
||||
import AdditionalInformation from 'components/product/additional-information';
|
||||
import { Gallery } from 'components/product/gallery';
|
||||
import { ProductDescription } from 'components/product/product-description';
|
||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||
@ -49,7 +50,13 @@ export async function generateMetadata({
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: { params: { handle: string } }) {
|
||||
export default async function ProductPage({
|
||||
params,
|
||||
searchParams
|
||||
}: {
|
||||
params: { handle: string };
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
const product = await getProduct(params.handle);
|
||||
|
||||
if (!product) return notFound();
|
||||
@ -86,6 +93,9 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
<div className="my-3 flex flex-col space-x-0 lg:flex-row lg:gap-8 lg:space-x-3">
|
||||
<div className="h-full w-full basis-full lg:basis-7/12">
|
||||
<ProductDescription product={product} />
|
||||
<Suspense>
|
||||
<AdditionalInformation product={product} searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block lg:basis-5/12">
|
||||
@ -95,7 +105,7 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
images={product.images.map((image: Image) => ({
|
||||
images={product.images.slice(0, 5).map((image: Image) => ({
|
||||
src: image.url,
|
||||
altText: image.altText
|
||||
}))}
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import Price from 'components/price';
|
||||
import { useDebounce } from 'hooks';
|
||||
import { useDebounce } from 'hooks/use-debounce';
|
||||
import { Filter } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import get from 'lodash.get';
|
||||
|
@ -40,7 +40,7 @@ const RichTextBlock = ({ block }: { block: Content }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-blue-200">
|
||||
<p className="text-black-700">
|
||||
{block.children.map((child, index) => (
|
||||
<RichTextBlock key={index} block={child} />
|
||||
))}
|
||||
@ -50,7 +50,7 @@ const RichTextBlock = ({ block }: { block: Content }) => {
|
||||
|
||||
const RichTextDisplay = ({ contentBlocks }: { contentBlocks: Content[] }) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{contentBlocks.map((block, index) => (
|
||||
<RichTextBlock key={index} block={block} />
|
||||
))}
|
||||
|
@ -8,7 +8,7 @@ const TextBlock = ({ block }: { block: Metaobject }) => {
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-5 px-4 md:px-0">
|
||||
{block.title && (
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{block.title}</h3>
|
||||
<h3 className="text-xl font-bold leading-6 text-black-700">{block.title}</h3>
|
||||
)}
|
||||
|
||||
<RichTextDisplay contentBlocks={content.children} />
|
||||
|
@ -1,14 +1,38 @@
|
||||
import PageContent from 'components/page/page-content';
|
||||
import { getMetaobject, getMetaobjectsByIds } from 'lib/shopify';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import Details from './details';
|
||||
import ShippingPolicy from './shipping-policy';
|
||||
import WarrantyPolicy from './warranty-policy';
|
||||
import { getSelectedProductVariant } from 'lib/utils';
|
||||
|
||||
const AdditionalInformation = async ({
|
||||
product,
|
||||
searchParams
|
||||
}: {
|
||||
product: Product;
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
}) => {
|
||||
const selectedVariant = getSelectedProductVariant({ product, searchParams });
|
||||
|
||||
if (!selectedVariant) return null;
|
||||
|
||||
const pdpContent = await getMetaobject({
|
||||
handle: {
|
||||
handle: `${selectedVariant.condition}-${product.productType}`.toLowerCase(),
|
||||
type: 'pdp_content'
|
||||
}
|
||||
});
|
||||
|
||||
if (!pdpContent) return null;
|
||||
|
||||
const contentIds = pdpContent.content ? JSON.parse(pdpContent.content) : [];
|
||||
const pageContent = await getMetaobjectsByIds(contentIds);
|
||||
|
||||
const AdditionalInformation = ({ product }: { product: Product }) => {
|
||||
return (
|
||||
<div className="my-5 w-full divide-y">
|
||||
<Details product={product} />
|
||||
<WarrantyPolicy />
|
||||
<ShippingPolicy />
|
||||
<div className="my-6 w-full divide-y">
|
||||
{pageContent.map((block) => (
|
||||
<div key={block.id} className="py-5">
|
||||
<PageContent block={block} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,82 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import Price from 'components/price';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import DisclosureSection from './disclosure-section';
|
||||
|
||||
const Details = ({ product }: { product: Product }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const variants = product.variants;
|
||||
|
||||
const variant = variants.find((variant) =>
|
||||
variant.selectedOptions.every(
|
||||
(option) => option.value === searchParams.get(option.name.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const details = [
|
||||
...(product.transmissionTag
|
||||
? [
|
||||
{
|
||||
title: 'Transmission Tag',
|
||||
value: product.transmissionTag.join()
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(product.transmissionCode
|
||||
? [
|
||||
{
|
||||
title: 'Transmission Code',
|
||||
value: product.transmissionCode.join()
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(product.transmissionSpeeds
|
||||
? [
|
||||
{
|
||||
title: 'Transmission Speeds',
|
||||
value: product.transmissionSpeeds.map((speed) => `${speed}-Speed`).join()
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
return (
|
||||
<DisclosureSection title="Product Details" defaultOpen>
|
||||
<div className="flex w-full items-center p-1">
|
||||
<span className="basis-2/5">Condition</span>
|
||||
<span>{variant?.condition || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex w-full items-center bg-gray-100 p-1">
|
||||
<span className="basis-2/5">Price</span>
|
||||
<Price
|
||||
amount={variant?.price.amount || product.priceRange.minVariantPrice.amount}
|
||||
currencyCode={
|
||||
variant?.price.currencyCode || product.priceRange.minVariantPrice.currencyCode
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center p-1">
|
||||
<span className="basis-2/5">Warranty</span>
|
||||
<span />
|
||||
</div>
|
||||
<div className="flex w-full items-center bg-gray-100 p-1">
|
||||
<span className="basis-2/5">Cylinders</span>
|
||||
<span>{product.engineCylinders?.map((cylinder) => `${cylinder} Cylinders`).join()}</span>
|
||||
</div>
|
||||
|
||||
{details.map(({ title, value }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx('flex w-full items-center p-1', { 'bg-gray-100': index % 2 !== 0 })}
|
||||
>
|
||||
<span className="basis-2/5">{title}</span>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</DisclosureSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default Details;
|
@ -1,25 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type DisclosureProps = {
|
||||
children: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const DisclosureSection = ({ children, title, defaultOpen }: DisclosureProps) => {
|
||||
return (
|
||||
<Disclosure as="div" className="p-3" defaultOpen={defaultOpen}>
|
||||
<DisclosureButton className="group flex w-full items-center justify-between">
|
||||
<span className="font-medium">{title}</span>
|
||||
<ChevronDownIcon className="size-4 group-data-[open]:rotate-180" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel className="mt-2 py-2 text-sm">{children}</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisclosureSection;
|
@ -2,7 +2,6 @@ import { AddToCart } from 'components/cart/add-to-cart';
|
||||
import Prose from 'components/prose';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { Suspense } from 'react';
|
||||
import AdditionalInformation from './additional-information';
|
||||
import CoreCharge from './core-charge';
|
||||
import Delivery from './delivery';
|
||||
import PriceSummary from './price-summary';
|
||||
@ -56,7 +55,6 @@ export function ProductDescription({ product }: { product: Product }) {
|
||||
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
|
||||
</Suspense>
|
||||
<SpecialOffer />
|
||||
<AdditionalInformation product={product} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
import DisclosureSection from './disclosure-section';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
const ShippingPolicy = () => {
|
||||
return (
|
||||
<DisclosureSection title="Shipping & returns">
|
||||
<p>
|
||||
At {SITE_NAME}, we offer a Flat Rate Shipping (Commercial address) service as long as the
|
||||
delivery address is in a commercially zoned location. Unfortunately, residential and home
|
||||
businesses are not considered commercial addresses. A business or commercial address
|
||||
location must be able to receive freight without the requirement of prior appointment setup
|
||||
or notification. This location should also have the capability of unloading the
|
||||
remanufactured transmission with a forklift from the delivery truck. If you don't have
|
||||
a commercial or business address that meets these specifications, you should ship it
|
||||
directly to the dealership or repair shop that is performing the repairs to ensure you enjoy
|
||||
Flat Rate Shipping (Commercial address). Residential delivery or Liftgate service will
|
||||
result in additional $99 fee.
|
||||
</p>
|
||||
<p className="my-3">
|
||||
After placing the order for a remanufactured transmission, most customers will receive it
|
||||
within 7-14 business days — not including holidays or weekends. Please keep in mind that
|
||||
certain locations (remote areas) and locations in Colorado, Utah, New York, Oregon, and
|
||||
California may require an additional delivery fee. In either case, we will always ship your
|
||||
remanufactured transmission out as soon as possible. Because of weather conditions,
|
||||
increasing order volumes, and conditions outside of our control, all shipping times are
|
||||
estimates, not guarantees. It's important to note that {SITE_NAME} will not be liable
|
||||
for any extra fees the carrier may levy due to storage or redelivery. While every
|
||||
transmission from {SITE_NAME} has been rigorously inspected and tested prior to being
|
||||
shipped, damage may occur during transportation.
|
||||
</p>
|
||||
<p>
|
||||
As such, we strongly suggest you carefully inspect your transmission upon receipt. If you
|
||||
notice any missing parts, wrong parts, or damage, you should report it prior to signing any
|
||||
delivery documentation. It"s imperative to report missing parts, damage, or wrong parts
|
||||
at the time of delivery. If you fail to do so prior to signing your shipping documents,
|
||||
responsibility will be placed on the purchaser or receiver. For clarity,
|
||||
"purchaser" refers to any representative of the company designated to sign for the
|
||||
delivery of the remanufactured transmission.
|
||||
</p>
|
||||
</DisclosureSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShippingPolicy;
|
@ -1,102 +0,0 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ArrowsRightLeftIcon,
|
||||
CurrencyDollarIcon,
|
||||
FlagIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import DisclosureSection from './disclosure-section';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
|
||||
const WarrantyPolicy = () => {
|
||||
return (
|
||||
<DisclosureSection title="Warranty">
|
||||
<div className="mb-3 font-medium">Year 2001 and Newer</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Personal/Individual Transmission Warranty</span>
|
||||
<span>60 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>Prior to 03/01/2020 18 Months/ 100,000 Miles</span>
|
||||
</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>Effective 03/01/2020 36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Continuously Variable Transmission (CVT) Warranty</span>
|
||||
<span>36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Manual Transmission Warranty</span>
|
||||
<span>36 Months/ Unlimited Miles</span>
|
||||
</div>
|
||||
<div className="my-3 font-medium">Year 2000 and Older</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Personal/Individual Transmission Warranty</span>
|
||||
<span>36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>18 Months/ 100,000 Miles</span>
|
||||
</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Continuously Variable Transmission (CVT) Warranty</span>
|
||||
<span>36 Months/ Unlimited Miles</span>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<ArrowsRightLeftIcon className="size-4 text-primary" />
|
||||
Easy, Hassle-Free, Transferable Warranty
|
||||
</div>
|
||||
<p>
|
||||
At {SITE_NAME}, we offer an easy, transferable, hassle-free warranty. Instead of being
|
||||
associated only with you, the warranty is attached to your Vehicle Identification Number.
|
||||
As such, the warranty is transferable with vehicle ownership, which means you never have
|
||||
to worry about any paperwork or fees involved. Please note, that the used parts warranty
|
||||
is not transferable.
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<FlagIcon className="size-4 text-primary" />
|
||||
Nationwide Coverage
|
||||
</div>
|
||||
<p>
|
||||
Whether you're in California, Chicago, New York, Florida, or anywhere in between, you
|
||||
are covered with a nationwide warranty. This warranty covers you anywhere in the
|
||||
continental U.S.
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<ArrowPathIcon className="size-4 text-primary" />
|
||||
Instant Replacement
|
||||
</div>
|
||||
<p>
|
||||
With instant replacement, your replacement transmission will be sent out as soon as you
|
||||
submit your claim. This way you can spend less time waiting and more time doing whatever
|
||||
needs to be done.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<CurrencyDollarIcon className="size-4 text-primary" />
|
||||
Paid Parts & Labor
|
||||
</div>
|
||||
<p>
|
||||
When you have your work performed in a certified shop, your {SITE_NAME} warranty will pay
|
||||
for parts and labor at $50 an hour, which is the Mitchell labor reimbursement rate.
|
||||
</p>
|
||||
</div>
|
||||
</DisclosureSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarrantyPolicy;
|
@ -9,6 +9,7 @@ const productFragment = /* GraphQL */ `
|
||||
title
|
||||
description
|
||||
descriptionHtml
|
||||
productType
|
||||
options {
|
||||
id
|
||||
name
|
||||
|
@ -522,6 +522,7 @@ export type ShopifyProduct = {
|
||||
title: string;
|
||||
description: string;
|
||||
descriptionHtml: string;
|
||||
productType: string;
|
||||
options: ProductOption[];
|
||||
priceRange: {
|
||||
maxVariantPrice: Money;
|
||||
|
@ -18,6 +18,9 @@ export const carPartPlanetColor = {
|
||||
200: '#666C89',
|
||||
500: '#2D3A7B',
|
||||
600: '#111C55'
|
||||
},
|
||||
black: {
|
||||
700: '#1A1A25'
|
||||
}
|
||||
};
|
||||
|
||||
@ -41,6 +44,9 @@ export const remanTransmissionColor = {
|
||||
200: '#666C89',
|
||||
500: '#2D3A7B',
|
||||
600: '#111C55'
|
||||
},
|
||||
black: {
|
||||
700: '#1A1A25'
|
||||
}
|
||||
};
|
||||
|
||||
@ -64,5 +70,8 @@ export const transmissionLocatorColor = {
|
||||
200: '#666C89',
|
||||
500: '#2D3A7B',
|
||||
600: '#111C55'
|
||||
},
|
||||
black: {
|
||||
700: '#1A1A25'
|
||||
}
|
||||
};
|
||||
|
18
lib/utils.ts
18
lib/utils.ts
@ -1,7 +1,7 @@
|
||||
import clsx, { ClassValue } from 'clsx';
|
||||
import { ReadonlyURLSearchParams } from 'next/navigation';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { Menu } from './shopify/types';
|
||||
import { Menu, Product, ProductVariant } from './shopify/types';
|
||||
|
||||
export function cx(...args: ClassValue[]) {
|
||||
return twMerge(clsx(...args));
|
||||
@ -149,3 +149,19 @@ export const getCollectionUrl = (handle: string, includeSlashPrefix = true) => {
|
||||
|
||||
return includeSlashPrefix ? `/${rewriteUrl}` : rewriteUrl;
|
||||
};
|
||||
|
||||
export const getSelectedProductVariant = ({
|
||||
product,
|
||||
searchParams
|
||||
}: {
|
||||
product: Product;
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
}) => {
|
||||
const variant = product.variants.find((variant: ProductVariant) =>
|
||||
variant.selectedOptions.every(
|
||||
(option) => option.value === searchParams?.[option.name.toLowerCase()]
|
||||
)
|
||||
);
|
||||
|
||||
return variant || product.variants[0];
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user