mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 15:36:58 +00:00
feat: playing around with styles and layout on PDP
Signed-off-by: jrphilo <james.philo@me.com>
This commit is contained in:
parent
80db45a522
commit
598c1ad53e
@ -86,11 +86,11 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
<div className="hidden lg:block">
|
||||
<BreadcrumbComponent type="product" handle={product.handle} />
|
||||
</div>
|
||||
<div className="my-3 flex flex-col space-x-0 rounded-lg border border-neutral-200 bg-white p-8 md:p-10 lg:flex-row lg:gap-8 lg:space-x-3 dark:border-neutral-800 dark:bg-black">
|
||||
<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="h-full w-full basis-full lg:basis-7/12">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden" />
|
||||
<div className="aspect-square relative h-full max-h-[550px] w-full overflow-hidden" />
|
||||
}
|
||||
>
|
||||
<Gallery
|
||||
|
@ -12,17 +12,28 @@ const Price = ({
|
||||
currencyCode: string;
|
||||
currencyCodeClassName?: string;
|
||||
showCurrency?: boolean;
|
||||
} & React.ComponentProps<'p'>) => (
|
||||
<p suppressHydrationWarning={true} className={className}>
|
||||
{`${new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
currencyDisplay: 'narrowSymbol'
|
||||
}).format(parseFloat(amount))}`}
|
||||
{showCurrency && (
|
||||
<span className={clsx('ml-1 inline', currencyCodeClassName)}>{`${currencyCode}`}</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
} & React.ComponentProps<'p'>) => {
|
||||
// Convert string to float and check if it is zero
|
||||
const price = parseFloat(amount);
|
||||
|
||||
// Return 'Included' if price is 0
|
||||
if (price === 0) {
|
||||
return <p className={className}>Included</p>;
|
||||
}
|
||||
|
||||
// Otherwise, format and display the price
|
||||
return (
|
||||
<p suppressHydrationWarning={true} className={className}>
|
||||
{new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
currencyDisplay: 'narrowSymbol'
|
||||
}).format(price)}
|
||||
{showCurrency && (
|
||||
<span className={clsx('ml-1 inline', currencyCodeClassName)}>{currencyCode}</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default Price;
|
||||
|
@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowPathRoundedSquareIcon } from '@heroicons/react/24/outline';
|
||||
import Price from 'components/price';
|
||||
import { CORE_VARIANT_ID_KEY, CORE_WAIVER } from 'lib/constants';
|
||||
import { CoreChargeOption, ProductVariant } from 'lib/shopify/types';
|
||||
import { cn, createUrl } from 'lib/utils';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
type CoreChargeProps = {
|
||||
@ -34,13 +36,13 @@ const CoreCharge = ({ variants }: CoreChargeProps) => {
|
||||
|
||||
const coreChargeOptions = [
|
||||
waiverAvailable && {
|
||||
label: 'Core Waiver',
|
||||
label: 'Waive Core',
|
||||
value: CORE_WAIVER,
|
||||
price: { amount: 0, currencyCode: variant?.price.currencyCode }
|
||||
},
|
||||
coreVariantId &&
|
||||
coreCharge && {
|
||||
label: 'Core Charge',
|
||||
label: 'Pay Core Upfront',
|
||||
value: coreVariantId,
|
||||
price: coreCharge
|
||||
}
|
||||
@ -52,25 +54,35 @@ const CoreCharge = ({ variants }: CoreChargeProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col text-xs lg:text-sm">
|
||||
<div className="mb-2 text-base font-medium">Core Charge</div>
|
||||
<div className="mb-1 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">
|
||||
<ArrowPathRoundedSquareIcon className="h-5 w-5" />
|
||||
<span> Core charge </span>
|
||||
</div>
|
||||
<Link href="#" className="pl-2 text-blue-800 hover:underline">
|
||||
Understanding Core Charges and Returns
|
||||
</Link>
|
||||
</div>
|
||||
{/*
|
||||
Plan is to move this to within the a modal tht opens when a user clicks on Understanding Core Charges and Returns
|
||||
<p className="mb-2 text-sm tracking-tight text-neutral-500">
|
||||
The core charge is a refundable deposit that is added to the price of the part. This charge
|
||||
ensures that the old, worn-out part is returned to the supplier for proper disposal or
|
||||
recycling. When you return the old part, you'll receive a refund of the core charge.
|
||||
</p>
|
||||
</p> */}
|
||||
<ul className="flex min-h-16 flex-row space-x-4 pt-2">
|
||||
{coreChargeOptions.map((option) => (
|
||||
<li className="flex w-32" key={option.value}>
|
||||
<button
|
||||
onClick={() => handleSelectCoreChargeOption(option.value)}
|
||||
className={cn(
|
||||
'flex w-full flex-col flex-wrap items-center justify-center space-y-2 rounded-md border p-2 text-center text-xs font-medium',
|
||||
'font-base flex w-full flex-col flex-wrap items-center justify-center space-y-0.5 rounded border text-center text-xs',
|
||||
{
|
||||
'ring-2 ring-secondary': coreVariantIdSearchParam === option.value
|
||||
'border-0 ring-2 ring-secondary': coreVariantIdSearchParam === option.value
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<span className="font-bold">{option.label}</span>
|
||||
<Price {...option.price} />
|
||||
</button>
|
||||
</li>
|
||||
|
@ -28,7 +28,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative aspect-1 h-full max-h-[550px] w-full overflow-hidden">
|
||||
<div className="relative hidden aspect-1 h-full max-h-[550px] w-full overflow-hidden md:block">
|
||||
{images[imageIndex] && (
|
||||
<Image
|
||||
className="h-full w-full object-contain"
|
||||
@ -70,7 +70,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
</div>
|
||||
|
||||
{images.length > 1 ? (
|
||||
<ul className="my-12 flex items-center justify-center gap-2 overflow-auto py-1 lg:mb-0">
|
||||
<ul className="mb-4 flex gap-2 overflow-auto py-1 sm:justify-start md:my-12 md:items-center md:justify-center lg:mb-0">
|
||||
{images.map((image, index) => {
|
||||
const isActive = index === imageIndex;
|
||||
const imageSearchParams = new URLSearchParams(searchParams.toString());
|
||||
@ -78,7 +78,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
imageSearchParams.set('image', index.toString());
|
||||
|
||||
return (
|
||||
<li key={image.src} className="h-20 w-20">
|
||||
<li key={image.src} className="h-16 w-16 md:h-20 md:w-20">
|
||||
<Link
|
||||
aria-label="Enlarge product image"
|
||||
href={createUrl(pathname, imageSearchParams)}
|
||||
|
22
components/product/price-in-cart.tsx
Normal file
22
components/product/price-in-cart.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
const PriceInCart = ({ items }) => {
|
||||
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
|
||||
|
||||
return (
|
||||
<div className="mt-4 border-t border-neutral-300 p-4">
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index} className="flex justify-between">
|
||||
<span>{item.label}</span>
|
||||
<span>${item.price.toFixed(2)}</span>
|
||||
</li>
|
||||
))}
|
||||
<li className="mt-2 flex justify-between border-t border-neutral-300 pt-2 font-bold">
|
||||
<span>Total In Cart</span>
|
||||
<span>${totalPrice.toFixed(2)}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceInCart;
|
@ -12,7 +12,11 @@ export function ProductDescription({ product }: { product: Product }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 flex flex-col dark:border-neutral-700">
|
||||
<h1 className="mb-3 text-2xl font-bold">{product.title}</h1>
|
||||
<h1 className="text-xl font-bold md:text-2xl">{product.title}</h1>
|
||||
<div className="mb-5 flex items-center justify-start gap-x-2">
|
||||
<p className="text-sm">SKU: 123456</p>
|
||||
<p className="text-sm">Condition: Used</p>
|
||||
</div>
|
||||
<VariantPrice
|
||||
variants={product.variants}
|
||||
defaultPrice={product.priceRange.minVariantPrice}
|
||||
@ -33,11 +37,11 @@ export function ProductDescription({ product }: { product: Product }) {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="mb-4 border-t pb-4 pt-6 dark:border-neutral-700">
|
||||
<div className="mb-2 border-t py-4 dark:border-neutral-700">
|
||||
<CoreCharge variants={product.variants} />
|
||||
</div>
|
||||
|
||||
<div className="mb-4 border-t py-6 dark:border-neutral-700">
|
||||
<div className="mb-2 border-t py-4 dark:border-neutral-700">
|
||||
<Warranty />
|
||||
</div>
|
||||
|
||||
|
@ -75,8 +75,8 @@ export function VariantSelector({
|
||||
const closeModal = () => setIsOpen(false);
|
||||
|
||||
return (
|
||||
<div className="mb-6 flex flex-row gap-1 text-sm font-medium">
|
||||
More Remanufactured and Used Options{' '}
|
||||
<div className="mb-6 flex flex-row gap-1 rounded-md border p-2 text-sm font-medium">
|
||||
See more Remanufactured and Used Options{' '}
|
||||
<button
|
||||
className="flex flex-row gap-0.5 font-normal text-blue-800 hover:underline"
|
||||
aria-label="Open variants selector"
|
||||
|
@ -13,22 +13,17 @@ const plans: Array<{
|
||||
price: number;
|
||||
}> = [
|
||||
{
|
||||
template: (
|
||||
<>
|
||||
<span>Included</span>
|
||||
<span>3-Year Warranty</span>
|
||||
</>
|
||||
),
|
||||
template: <span className="font-bold">3-Year Warranty</span>,
|
||||
price: 0,
|
||||
key: 'Included'
|
||||
},
|
||||
{
|
||||
template: <span>Premium Labor</span>,
|
||||
template: <span className="font-bold">Premium Labor</span>,
|
||||
price: 150,
|
||||
key: 'Premium Labor'
|
||||
},
|
||||
{
|
||||
template: <span>+1 Year</span>,
|
||||
template: <span className="font-bold">+1 Year</span>,
|
||||
price: 100,
|
||||
key: '+1 Year'
|
||||
}
|
||||
@ -42,9 +37,9 @@ const WarrantySelector = () => {
|
||||
<button
|
||||
onClick={() => setSelectedOptions(plan.key)}
|
||||
className={cn(
|
||||
'flex w-full flex-col flex-wrap items-center justify-center space-y-2 rounded-md border p-2 text-center text-xs font-medium',
|
||||
'font-base flex w-full flex-col flex-wrap items-center justify-center space-y-0.5 rounded border text-center text-xs',
|
||||
{
|
||||
'ring-2 ring-secondary': plan.key === selectedOptions
|
||||
'border-0 ring-2 ring-secondary': plan.key === selectedOptions
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
@ -5,18 +5,21 @@ import WarrantySelector from './warranty-selector';
|
||||
const Warranty = () => {
|
||||
return (
|
||||
<div className="flex flex-col text-xs lg:text-sm">
|
||||
<div className="mb-3 flex flex-row items-center space-x-2 text-base font-medium">
|
||||
<ShieldCheckIcon className="h-7 w-7" />
|
||||
<span> Protect your product</span>
|
||||
</div>
|
||||
<div className="mb-1 flex flex-row items-center space-x-1 divide-x divide-gray-400 leading-none lg:space-x-3">
|
||||
<span>Extended Warranty</span>
|
||||
<Link href="#" className="pl-2 text-blue-800 hover:underline">
|
||||
What's Included
|
||||
</Link>
|
||||
<Link href="#" className="pl-2 text-blue-800 hover:underline">
|
||||
Terms & Conditions
|
||||
</Link>
|
||||
<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">
|
||||
<ShieldCheckIcon className="h-5 w-5" />
|
||||
<span>Warranty</span>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
<Link href="#" className="text-xs text-blue-800 hover:underline lg:text-sm">
|
||||
What's Included
|
||||
</Link>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
<Link href="#" className="text-xs text-blue-800 hover:underline lg:text-sm">
|
||||
Terms & Conditions
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<WarrantySelector />
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user