fix: update PDP layout

Signed-off-by: Chloe <vanguyen.work@gmail.com>
This commit is contained in:
Chloe
2024-06-17 11:04:43 +07:00
parent 3ac4b140c9
commit a11287d4ad
14 changed files with 390 additions and 46 deletions

View File

@@ -93,8 +93,8 @@ const CoreCharge = ({ variants }: CoreChargeProps) => {
period, you will never need to pay the core charge.
</p>
<p className="text-sm">
If you don't manage to return the old part within the 30-day period, we will then
charge you the core charge. This keeps more money in your pocket upfront.
If you don&apos;t manage to return the old part within the 30-day period, we will
then charge you the core charge. This keeps more money in your pocket upfront.
</p>
</section>

View 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;

View 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;

View File

@@ -3,6 +3,9 @@ import Prose from 'components/prose';
import { Product } from 'lib/shopify/types';
import { Suspense } from 'react';
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 VariantDetails from './vairant-details';
import { VariantSelector } from './variant-selector';
@@ -11,7 +14,7 @@ import Warranty from './warranty';
export function ProductDescription({ product }: { product: Product }) {
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>
<VariantDetails
@@ -34,6 +37,7 @@ export function ProductDescription({ product }: { product: Product }) {
/>
) : null}
<ProductDetails product={product} />
<div className="mb-2 border-t py-4 dark:border-neutral-700">
<CoreCharge variants={product.variants} />
</div>
@@ -42,12 +46,15 @@ export function ProductDescription({ product }: { product: Product }) {
<Warranty />
</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}>
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
</Suspense>
<div className="mt-4 border-t pt-4">
<SpecialOffer />
</div>
<SpecialOffer />
</>
);
}

View 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;

View File

@@ -1,28 +1,71 @@
import { CurrencyDollarIcon, ShieldCheckIcon, UsersIcon } from '@heroicons/react/24/outline';
import { TruckIcon } from '@heroicons/react/24/solid';
import {
ArrowPathIcon,
CurrencyDollarIcon,
ShieldCheckIcon,
StarIcon,
TruckIcon,
UsersIcon
} from '@heroicons/react/24/outline';
const SpecialOffer = () => {
return (
<>
<div className="mb-3 text-base font-medium tracking-tight">Special Offers</div>
<div className="flex flex-col space-y-2 pl-2 text-sm tracking-normal text-neutral-800 lg:text-base dark:text-white">
<p className="flex items-center gap-3">
<TruckIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Flat Rate Shipping
(Commercial Address)
</p>
<p className="flex items-center gap-3">
<ShieldCheckIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Up to 5 Years
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 className="mt-10 grid grid-cols-2 gap-y-5 xl:grid-cols-3">
<div className="flex items-start gap-3">
<TruckIcon className="size-12 text-primary" />
<div className="flex flex-col">
<span className="font-medium uppercase">Flat Rate Shipping</span>
<span className="text-sm font-light">
We offer a flat $299 shipping fee to commercial addresses
</span>
</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&apos;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>
);
};

View File

@@ -1,5 +1,6 @@
'use client';
import { CheckCircleIcon } from '@heroicons/react/24/outline';
import Price from 'components/price';
import { Money, ProductVariant } from 'lib/shopify/types';
import { useSearchParams } from 'next/navigation';
@@ -20,17 +21,23 @@ const VariantDetails = ({ variants, defaultPrice }: VariantDetailsProps) => {
const price = variant?.price.amount || defaultPrice.amount;
return (
<>
<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>
<div className="mt-1">
<Price
amount={price}
currencyCode={variant?.price.currencyCode || defaultPrice.currencyCode}
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>
);
};