mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 20:57:51 +00:00
fix: update PDP layout
Signed-off-by: Chloe <vanguyen.work@gmail.com>
This commit is contained in:
parent
3ac4b140c9
commit
a11287d4ad
@ -85,8 +85,12 @@ export default async function ProductPage({ params }: { params: { handle: string
|
|||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<BreadcrumbComponent type="product" handle={product.handle} />
|
<BreadcrumbComponent type="product" handle={product.handle} />
|
||||||
</div>
|
</div>
|
||||||
<div className="my-3 flex flex-col space-x-0 rounded-lg border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-black md:p-10 lg:flex-row lg:gap-8 lg:space-x-3">
|
<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">
|
<div className="h-full w-full basis-full lg:basis-7/12">
|
||||||
|
<ProductDescription product={product} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="basis-full lg:basis-5/12">
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="aspect-square relative h-full max-h-[550px] w-full overflow-hidden" />
|
<div className="aspect-square relative h-full max-h-[550px] w-full overflow-hidden" />
|
||||||
@ -100,18 +104,12 @@ export default async function ProductPage({ params }: { params: { handle: string
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="basis-full lg:basis-5/12">
|
|
||||||
<ProductDescription product={product} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<RelatedProducts id={product.id} />
|
<RelatedProducts id={product.id} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<Suspense>
|
<Footer />
|
||||||
<Footer />
|
|
||||||
</Suspense>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ function SubmitButton({
|
|||||||
}) {
|
}) {
|
||||||
const { pending } = useFormStatus();
|
const { pending } = useFormStatus();
|
||||||
const buttonClasses =
|
const buttonClasses =
|
||||||
'relative flex w-full items-center justify-center rounded bg-secondary p-4 tracking-wide text-white gap-3';
|
'relative flex w-full items-center justify-center rounded bg-secondary p-3 tracking-wide text-white gap-3';
|
||||||
const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
|
const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
|
||||||
|
|
||||||
if (!availableForSale) {
|
if (!availableForSale) {
|
||||||
|
@ -5,13 +5,15 @@ const Price = ({
|
|||||||
className,
|
className,
|
||||||
currencyCode = 'USD',
|
currencyCode = 'USD',
|
||||||
currencyCodeClassName,
|
currencyCodeClassName,
|
||||||
showCurrency = false
|
showCurrency = false,
|
||||||
|
prefix
|
||||||
}: {
|
}: {
|
||||||
amount: string;
|
amount: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
currencyCodeClassName?: string;
|
currencyCodeClassName?: string;
|
||||||
showCurrency?: boolean;
|
showCurrency?: boolean;
|
||||||
|
prefix?: string;
|
||||||
} & React.ComponentProps<'p'>) => {
|
} & React.ComponentProps<'p'>) => {
|
||||||
// Convert string to float and check if it is zero
|
// Convert string to float and check if it is zero
|
||||||
const price = parseFloat(amount);
|
const price = parseFloat(amount);
|
||||||
@ -24,6 +26,7 @@ const Price = ({
|
|||||||
// Otherwise, format and display the price
|
// Otherwise, format and display the price
|
||||||
return (
|
return (
|
||||||
<p suppressHydrationWarning={true} className={className}>
|
<p suppressHydrationWarning={true} className={className}>
|
||||||
|
{prefix}
|
||||||
{new Intl.NumberFormat(undefined, {
|
{new Intl.NumberFormat(undefined, {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: currencyCode,
|
currency: currencyCode,
|
||||||
|
@ -93,8 +93,8 @@ const CoreCharge = ({ variants }: CoreChargeProps) => {
|
|||||||
period, you will never need to pay the core charge.
|
period, you will never need to pay the core charge.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
If you don't manage to return the old part within the 30-day period, we will then
|
If you don't manage to return the old part within the 30-day period, we will
|
||||||
charge you the core charge. This keeps more money in your pocket upfront.
|
then charge you the core charge. This keeps more money in your pocket upfront.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
114
components/product/delivery.tsx
Normal file
114
components/product/delivery.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { TruckIcon } from '@heroicons/react/24/outline';
|
||||||
|
import Price from 'components/price';
|
||||||
|
import SideDialog from 'components/side-dialog';
|
||||||
|
import { DELIVERY_OPTION_KEY } from 'lib/constants';
|
||||||
|
import { cn, createUrl } from 'lib/utils';
|
||||||
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
|
const options = ['Commercial', 'Residential'] as const;
|
||||||
|
type Option = (typeof options)[number];
|
||||||
|
|
||||||
|
export const deliveryOptions: Array<{
|
||||||
|
key: Option;
|
||||||
|
template: ReactNode;
|
||||||
|
price: number;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
template: <span className="font-bold">Commercial</span>,
|
||||||
|
price: 299,
|
||||||
|
key: 'Commercial'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
template: <span className="font-bold">Residential</span>,
|
||||||
|
price: 398,
|
||||||
|
key: 'Residential'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const Delivery = () => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [openingDialog, setOpeningDialog] = useState<'information' | 'terms-conditions' | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const newSearchParams = new URLSearchParams(searchParams.toString());
|
||||||
|
const selectedDeliveryOption = newSearchParams.get(DELIVERY_OPTION_KEY);
|
||||||
|
|
||||||
|
const handleSelectDelivery = (option: Option) => {
|
||||||
|
newSearchParams.set(DELIVERY_OPTION_KEY, option);
|
||||||
|
|
||||||
|
const newUrl = createUrl(pathname, newSearchParams);
|
||||||
|
router.replace(newUrl, { scroll: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!selectedDeliveryOption) {
|
||||||
|
handleSelectDelivery(options[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col text-xs lg:text-sm">
|
||||||
|
<div className="mb-3 flex flex-row items-center space-x-1 divide-x divide-gray-400 leading-none lg:space-x-3">
|
||||||
|
<div className="flex flex-row items-center space-x-2 text-base font-medium">
|
||||||
|
<TruckIcon className="h-5 w-5" />
|
||||||
|
<span>Delivery</span>
|
||||||
|
</div>
|
||||||
|
<div className="pl-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpeningDialog('information')}
|
||||||
|
className="text-xs text-blue-800 hover:underline lg:text-sm"
|
||||||
|
>
|
||||||
|
Information
|
||||||
|
</button>
|
||||||
|
<SideDialog
|
||||||
|
title="Information"
|
||||||
|
onClose={() => setOpeningDialog(null)}
|
||||||
|
open={openingDialog === 'information'}
|
||||||
|
>
|
||||||
|
<p>Information</p>
|
||||||
|
</SideDialog>
|
||||||
|
</div>
|
||||||
|
<div className="pl-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpeningDialog('terms-conditions')}
|
||||||
|
className="text-xs text-blue-800 hover:underline lg:text-sm"
|
||||||
|
>
|
||||||
|
Terms & Conditions
|
||||||
|
</button>
|
||||||
|
<SideDialog
|
||||||
|
title="Terms & Conditions"
|
||||||
|
onClose={() => setOpeningDialog(null)}
|
||||||
|
open={openingDialog === 'terms-conditions'}
|
||||||
|
>
|
||||||
|
<p>Terms & Conditions</p>
|
||||||
|
</SideDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="flex min-h-16 flex-row space-x-4 pt-2">
|
||||||
|
{deliveryOptions.map((option) => (
|
||||||
|
<li className="flex w-32" key={option.key}>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelectDelivery(option.key)}
|
||||||
|
className={cn(
|
||||||
|
'font-base flex w-full flex-col flex-wrap items-center justify-center space-y-0.5 rounded border text-center text-xs',
|
||||||
|
{
|
||||||
|
'border-0 ring-2 ring-secondary': selectedDeliveryOption === option.key
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.template}
|
||||||
|
<Price amount={String(option.price)} currencyCode="USD" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Delivery;
|
73
components/product/price-summary.tsx
Normal file
73
components/product/price-summary.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Price from 'components/price';
|
||||||
|
import { CORE_VARIANT_ID_KEY, CORE_WAIVER, DELIVERY_OPTION_KEY } from 'lib/constants';
|
||||||
|
import { Money, ProductVariant } from 'lib/shopify/types';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { deliveryOptions } from './delivery';
|
||||||
|
|
||||||
|
type PriceSummaryProps = {
|
||||||
|
variants: ProductVariant[];
|
||||||
|
defaultPrice: Money;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PriceSummary = ({ variants, defaultPrice }: PriceSummaryProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const variant = variants.find((variant) =>
|
||||||
|
variant.selectedOptions.every(
|
||||||
|
(option) => option.value === searchParams.get(option.name.toLowerCase())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const price = variant?.price.amount || defaultPrice.amount;
|
||||||
|
const selectedCoreChargeOption = searchParams.get(CORE_VARIANT_ID_KEY);
|
||||||
|
const selectedDeliveryOption = searchParams.get(DELIVERY_OPTION_KEY);
|
||||||
|
const deliveryPrice =
|
||||||
|
deliveryOptions.find((option) => option.key === selectedDeliveryOption)?.price ?? 0;
|
||||||
|
const currencyCode = variant?.price.currencyCode || defaultPrice.currencyCode;
|
||||||
|
const corePrice = selectedCoreChargeOption === CORE_WAIVER ? 0 : variant?.coreCharge?.amount ?? 0;
|
||||||
|
|
||||||
|
const totalPrice = Number(price) + deliveryPrice + Number(corePrice);
|
||||||
|
return (
|
||||||
|
<div className="mb-3 flex flex-col gap-2">
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<span className="text-xl font-semibold">Our Price</span>
|
||||||
|
<Price amount={price} currencyCode={currencyCode} className="text-2xl font-semibold" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-400">{`Core Charge ${selectedCoreChargeOption === CORE_WAIVER ? '(Waived for 30 days)' : ''}`}</span>
|
||||||
|
{selectedCoreChargeOption === CORE_WAIVER ? (
|
||||||
|
<span className="text-sm text-gray-400">{`+$0.00`}</span>
|
||||||
|
) : (
|
||||||
|
<Price
|
||||||
|
amount={variant?.coreCharge?.amount ?? '0'}
|
||||||
|
currencyCode={currencyCode}
|
||||||
|
className="text-sm text-gray-400"
|
||||||
|
prefix="+"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-400">{`Flat Rate Shipping (${selectedDeliveryOption} address)`}</span>
|
||||||
|
<Price
|
||||||
|
amount={String(deliveryPrice)}
|
||||||
|
currencyCode={currencyCode}
|
||||||
|
className="text-sm text-gray-400"
|
||||||
|
prefix="+"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<span className="text-sm text-gray-400">To Pay Today</span>
|
||||||
|
<Price
|
||||||
|
amount={String(totalPrice)}
|
||||||
|
currencyCode={currencyCode}
|
||||||
|
className="text-sm text-gray-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PriceSummary;
|
@ -3,6 +3,9 @@ import Prose from 'components/prose';
|
|||||||
import { Product } from 'lib/shopify/types';
|
import { Product } from 'lib/shopify/types';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import CoreCharge from './core-charge';
|
import CoreCharge from './core-charge';
|
||||||
|
import Delivery from './delivery';
|
||||||
|
import PriceSummary from './price-summary';
|
||||||
|
import ProductDetails from './product-details';
|
||||||
import SpecialOffer from './special-offer';
|
import SpecialOffer from './special-offer';
|
||||||
import VariantDetails from './vairant-details';
|
import VariantDetails from './vairant-details';
|
||||||
import { VariantSelector } from './variant-selector';
|
import { VariantSelector } from './variant-selector';
|
||||||
@ -11,7 +14,7 @@ import Warranty from './warranty';
|
|||||||
export function ProductDescription({ product }: { product: Product }) {
|
export function ProductDescription({ product }: { product: Product }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-5 flex flex-col dark:border-neutral-700">
|
<div className="mb-4 flex flex-col">
|
||||||
<h1 className="text-xl font-bold md:text-2xl">{product.title}</h1>
|
<h1 className="text-xl font-bold md:text-2xl">{product.title}</h1>
|
||||||
|
|
||||||
<VariantDetails
|
<VariantDetails
|
||||||
@ -34,6 +37,7 @@ export function ProductDescription({ product }: { product: Product }) {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<ProductDetails product={product} />
|
||||||
<div className="mb-2 border-t py-4 dark:border-neutral-700">
|
<div className="mb-2 border-t py-4 dark:border-neutral-700">
|
||||||
<CoreCharge variants={product.variants} />
|
<CoreCharge variants={product.variants} />
|
||||||
</div>
|
</div>
|
||||||
@ -42,12 +46,15 @@ export function ProductDescription({ product }: { product: Product }) {
|
|||||||
<Warranty />
|
<Warranty />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2 border-t py-4 dark:border-neutral-700">
|
||||||
|
<Delivery />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PriceSummary variants={product.variants} defaultPrice={product.priceRange.minVariantPrice} />
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
|
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<div className="mt-4 border-t pt-4">
|
<SpecialOffer />
|
||||||
<SpecialOffer />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
51
components/product/product-details.tsx
Normal file
51
components/product/product-details.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
BeakerIcon,
|
||||||
|
BoltIcon,
|
||||||
|
CogIcon,
|
||||||
|
CpuChipIcon,
|
||||||
|
CubeTransparentIcon
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { Product } from 'lib/shopify/types';
|
||||||
|
|
||||||
|
const ProductDetails = ({ product }: { product: Product }) => {
|
||||||
|
return (
|
||||||
|
<div className="mb-3 flex flex-col gap-3">
|
||||||
|
<span className="font-medium">Details</span>
|
||||||
|
<div className="grid grid-cols-4 gap-y-3 text-sm">
|
||||||
|
{product.transmissionType && (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<CubeTransparentIcon className="size-4 text-primary" />
|
||||||
|
{product.transmissionType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{product.transmissionSpeeds && product.transmissionSpeeds.length && (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<BoltIcon className="size-4 text-primary" />
|
||||||
|
{`${product.transmissionSpeeds[0]}-Speed`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product.driveType && (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<CogIcon className="size-4 text-primary" />
|
||||||
|
{product.driveType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product.engineCylinders?.length && (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<BeakerIcon className="size-4 text-primary" />
|
||||||
|
{`${product.engineCylinders[0]} Cylinders`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product.transmissionCode?.length && (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<CpuChipIcon className="size-4 text-primary" />
|
||||||
|
{product.transmissionCode[0]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductDetails;
|
@ -1,28 +1,71 @@
|
|||||||
import { CurrencyDollarIcon, ShieldCheckIcon, UsersIcon } from '@heroicons/react/24/outline';
|
import {
|
||||||
import { TruckIcon } from '@heroicons/react/24/solid';
|
ArrowPathIcon,
|
||||||
|
CurrencyDollarIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
StarIcon,
|
||||||
|
TruckIcon,
|
||||||
|
UsersIcon
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
const SpecialOffer = () => {
|
const SpecialOffer = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="mt-10 grid grid-cols-2 gap-y-5 xl:grid-cols-3">
|
||||||
<div className="mb-3 text-base font-medium tracking-tight">Special Offers</div>
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex flex-col space-y-2 pl-2 text-sm tracking-normal text-neutral-800 lg:text-base dark:text-white">
|
<TruckIcon className="size-12 text-primary" />
|
||||||
<p className="flex items-center gap-3">
|
<div className="flex flex-col">
|
||||||
<TruckIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Flat Rate Shipping
|
<span className="font-medium uppercase">Flat Rate Shipping</span>
|
||||||
(Commercial Address)
|
<span className="text-sm font-light">
|
||||||
</p>
|
We offer a flat $299 shipping fee to commercial addresses
|
||||||
<p className="flex items-center gap-3">
|
</span>
|
||||||
<ShieldCheckIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Up to 5 Years
|
</div>
|
||||||
Unlimited Miles Warranty
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center gap-3">
|
|
||||||
<UsersIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Excellent Customer Support
|
|
||||||
</p>
|
|
||||||
<p className="flex items-center gap-3">
|
|
||||||
<CurrencyDollarIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> No Core Charge for
|
|
||||||
30 days
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="flex items-start gap-3">
|
||||||
|
<CurrencyDollarIcon className="size-10 text-primary" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium uppercase">Best Price Guarantee</span>
|
||||||
|
<span className="text-sm font-light">
|
||||||
|
We will match or beat any competitor's pricing
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<ShieldCheckIcon className="size-8 text-primary" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium uppercase">Unbeatable Warranty</span>
|
||||||
|
<span className="text-sm font-light">Up to 5 years with unlimited miles</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<UsersIcon className="size-10 text-primary" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium uppercase">Excellent Support</span>
|
||||||
|
<span className="text-sm font-light">
|
||||||
|
End-to-end, expert care from our customer service team
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<ArrowPathIcon className="size-10 text-primary" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium uppercase">Core Charge Waiver</span>
|
||||||
|
<span className="text-sm font-light">
|
||||||
|
Avoid the core charge by returning within 30 days
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<StarIcon className="size-10 text-primary" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium uppercase">Free Core Return</span>
|
||||||
|
<span className="text-sm font-light">
|
||||||
|
Unlike competitors, we pay for the return of your core
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { CheckCircleIcon } from '@heroicons/react/24/outline';
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import { Money, ProductVariant } from 'lib/shopify/types';
|
import { Money, ProductVariant } from 'lib/shopify/types';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
@ -20,17 +21,23 @@ const VariantDetails = ({ variants, defaultPrice }: VariantDetailsProps) => {
|
|||||||
const price = variant?.price.amount || defaultPrice.amount;
|
const price = variant?.price.amount || defaultPrice.amount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="mt-1">
|
||||||
<div className="mb-5 flex items-center justify-start gap-x-2">
|
|
||||||
<p className="text-sm">SKU: {variant?.sku || 'N/A'}</p>
|
|
||||||
<p className="text-sm">Condition: {variant?.condition || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
<Price
|
<Price
|
||||||
amount={price}
|
amount={price}
|
||||||
currencyCode={variant?.price.currencyCode || defaultPrice.currencyCode}
|
currencyCode={variant?.price.currencyCode || defaultPrice.currencyCode}
|
||||||
className="text-2xl font-semibold"
|
className="text-2xl font-semibold"
|
||||||
/>
|
/>
|
||||||
</>
|
<div className="mt-2 flex items-center justify-start gap-x-2">
|
||||||
|
{variant?.availableForSale ? (
|
||||||
|
<div className="flex items-center gap-1 text-sm text-green-500">
|
||||||
|
<CheckCircleIcon className="size-5" /> In Stock
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-red-600">Out of Stock</span>
|
||||||
|
)}
|
||||||
|
<p className="text-sm">Condition: {variant?.condition || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,3 +51,5 @@ export const CONDITIONS = {
|
|||||||
Used: 'Used',
|
Used: 'Used',
|
||||||
Remanufactured: 'Remanufactured'
|
Remanufactured: 'Remanufactured'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DELIVERY_OPTION_KEY = 'delivery';
|
||||||
|
@ -76,6 +76,21 @@ const productFragment = /* GraphQL */ `
|
|||||||
fuelType: metafield(namespace: "custom", key: "fuel") {
|
fuelType: metafield(namespace: "custom", key: "fuel") {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
transmissionType: metafield(namespace: "custom", key: "transmission_type") {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
transmissionSpeeds: metafield(namespace: "custom", key: "transmission_speeds") {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
driveType: metafield(namespace: "custom", key: "drive_type") {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
transmissionCode: metafield(namespace: "custom", key: "transmission_code") {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
transmissionTag: metafield(namespace: "custom", key: "transmission_tag") {
|
||||||
|
value
|
||||||
|
}
|
||||||
images(first: 20) {
|
images(first: 20) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
|
@ -75,7 +75,8 @@ import {
|
|||||||
ShopifyProductsOperation,
|
ShopifyProductsOperation,
|
||||||
ShopifyRemoveFromCartOperation,
|
ShopifyRemoveFromCartOperation,
|
||||||
ShopifySetCartAttributesOperation,
|
ShopifySetCartAttributesOperation,
|
||||||
ShopifyUpdateCartOperation
|
ShopifyUpdateCartOperation,
|
||||||
|
TransmissionType
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||||
@ -287,7 +288,10 @@ const reshapeVariants = (variants: ShopifyProductVariant[]): ProductVariant[] =>
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => {
|
const reshapeProduct = (
|
||||||
|
product: ShopifyProduct,
|
||||||
|
filterHiddenProducts: boolean = true
|
||||||
|
): Product | undefined => {
|
||||||
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
|
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -295,6 +299,13 @@ const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean =
|
|||||||
const { images, variants, ...rest } = product;
|
const { images, variants, ...rest } = product;
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
|
transmissionCode: parseMetaFieldValue<string[]>(product.transmissionCode),
|
||||||
|
transmissionSpeeds: parseMetaFieldValue<number[]>(product.transmissionSpeeds),
|
||||||
|
transmissionTag: parseMetaFieldValue<string[]>(product.transmissionTag),
|
||||||
|
driveType: parseMetaFieldValue<string[]>(product.driveType),
|
||||||
|
transmissionType: product.transmissionType
|
||||||
|
? (product.transmissionType.value as TransmissionType)
|
||||||
|
: null,
|
||||||
engineCylinders: parseMetaFieldValue<number[]>(product.engineCylinders),
|
engineCylinders: parseMetaFieldValue<number[]>(product.engineCylinders),
|
||||||
fuelType: product.fuelType?.value || null,
|
fuelType: product.fuelType?.value || null,
|
||||||
images: reshapeImages(images, product.title),
|
images: reshapeImages(images, product.title),
|
||||||
|
@ -101,14 +101,29 @@ export type Metaobject = {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TransmissionType = 'Automatic' | 'Manual';
|
||||||
|
|
||||||
export type Product = Omit<
|
export type Product = Omit<
|
||||||
ShopifyProduct,
|
ShopifyProduct,
|
||||||
'variants' | 'images' | 'fuelType' | 'engineCylinders'
|
| 'variants'
|
||||||
|
| 'images'
|
||||||
|
| 'fuelType'
|
||||||
|
| 'engineCylinders'
|
||||||
|
| 'driveType'
|
||||||
|
| 'transmissionType'
|
||||||
|
| 'transmissionSpeeds'
|
||||||
|
| 'transmissionCode'
|
||||||
|
| 'transmissionTag'
|
||||||
> & {
|
> & {
|
||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
images: Image[];
|
images: Image[];
|
||||||
fuelType: string | null;
|
fuelType: string | null;
|
||||||
engineCylinders: number[] | null;
|
engineCylinders: number[] | null;
|
||||||
|
driveType: string[] | null;
|
||||||
|
transmissionType: TransmissionType | null;
|
||||||
|
transmissionSpeeds: number[] | null;
|
||||||
|
transmissionCode: string[] | null;
|
||||||
|
transmissionTag: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProductOption = {
|
export type ProductOption = {
|
||||||
@ -216,6 +231,11 @@ export type ShopifyProduct = {
|
|||||||
};
|
};
|
||||||
engineCylinders: { value: string } | null;
|
engineCylinders: { value: string } | null;
|
||||||
fuelType: { value: string } | null;
|
fuelType: { value: string } | null;
|
||||||
|
transmissionType: { value: string } | null;
|
||||||
|
transmissionTag: { value: string } | null;
|
||||||
|
transmissionCode: { value: string } | null;
|
||||||
|
driveType: { value: string } | null;
|
||||||
|
transmissionSpeeds: { value: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopifyCartOperation = {
|
export type ShopifyCartOperation = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user