feat: adding more information warranty, part number, sku, speciall offers

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-04-25 14:14:20 +07:00
parent e3f564ca77
commit 59c3f07beb
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
14 changed files with 215 additions and 85 deletions

View File

@ -84,8 +84,8 @@ export default async function ProductPage({ params }: { params: { handle: string
/>
<div className="mx-auto max-w-screen-2xl px-4">
<BreadcrumbComponent type="product" handle={product.handle} />
<div className="my-3 flex flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 lg:flex-row lg:gap-8 dark:border-neutral-800 dark:bg-black">
<div className="h-full w-full basis-full lg:basis-4/6">
<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="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" />
@ -100,7 +100,7 @@ export default async function ProductPage({ params }: { params: { handle: string
</Suspense>
</div>
<div className="basis-full lg:basis-2/6">
<div className="basis-full lg:basis-5/12">
<ProductDescription product={product} />
</div>
</div>

View File

@ -11,12 +11,10 @@ type CoreChargeBadgeProps = {
const CoreChargeBadge = ({ variants, selectedOptions }: CoreChargeBadgeProps) => {
const selectedOptionsMap = new Map(selectedOptions.map((option) => [option.name, option.value]));
console.log({ selectedOptionsMap, variants });
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every((option) => option.value === selectedOptionsMap.get(option.name))
);
console.log({ variant });
return <CoreCharge variant={variant} sm />;
};

View File

@ -183,7 +183,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
</div>
<a
href={cart.checkoutUrl}
className="block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
className="block w-full rounded-full bg-secondary p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
>
Proceed to Checkout
</a>

View File

@ -11,35 +11,36 @@ type CoreChargeProps = {
const CoreCharge = ({ variant, sm = false }: CoreChargeProps) => {
if (!variant || !variant.coreCharge?.amount || variant.waiverAvailable) return null;
const originalPrice = String(Number(variant.price.amount) - Number(variant.coreCharge.amount));
const coreChargeDisplay = (
<Price amount={variant.coreCharge.amount} currencyCode={variant.price.currencyCode} />
);
const originalPrice = String(Number(variant.price.amount) - Number(variant.coreCharge.amount));
return (
<div className="flex items-center gap-2 rounded-md bg-gray-100 px-3 py-1 text-sm">
<ArrowPathIcon className="h-4 w-4" />
<span className="flex items-center gap-1" data-tooltip-id="core-charge-explanation">
<ArrowPathIcon className="h-3 w-3" />
<span
className="flex items-center gap-1"
data-tooltip-id={!sm ? 'core-charge-explanation' : undefined}
>
{sm ? coreChargeDisplay : <>Core Charge: {coreChargeDisplay}</>}
</span>
{sm ? null : (
<Tooltip id="core-charge-explanation" className="max-w-64">
<p className="flex flex-wrap items-center gap-1">
The core charge of {coreChargeDisplay} is a refundable deposit that is added to the
price of the part.
<Tooltip id="core-charge-explanation" className="z-20 max-w-72">
<p className="flex flex-wrap items-center gap-1 text-sm">
The core charge of {coreChargeDisplay} is a refundable deposit that is added to the price
of the part.
</p>
<p>
<p className="text-sm">
This charge ensures that the old, worn-out part is returned to the supplier for proper
disposal or recycling.
</p>
<p className="flex flex-wrap items-center gap-1">
When you return the old part, you&apos;ll receive a refund of the core charge, making
the final price of the part{' '}
<p className="flex flex-wrap items-center gap-1 text-sm">
When you return the old part, you&apos;ll receive a refund of the core charge, making the
final price of the part
<Price amount={originalPrice} currencyCode={variant.price.currencyCode} />
</p>
</Tooltip>
)}
</div>
);
};

View File

@ -39,10 +39,10 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
priority={true}
/>
)}
{images.length > 1 ? (
<>
<div className="absolute bottom-[15%] flex w-full justify-center">
<div className="mx-auto flex h-11 items-center rounded-full border border-white bg-neutral-50/80 text-neutral-500 backdrop-blur dark:border-black dark:bg-neutral-900/80">
<div className="mx-auto mb-3 flex h-11 items-center rounded-full border border-white bg-neutral-50/80 text-neutral-500 backdrop-blur dark:border-black dark:bg-neutral-900/80">
<Link
aria-label="Previous product image"
href={previousUrl}
@ -62,6 +62,10 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
</Link>
</div>
</div>
<p className="absolute bottom-[5%] flex w-full justify-center text-sm text-neutral-500">
Representative Image
</p>
</>
) : null}
</div>

View File

@ -1,10 +1,7 @@
'use client';
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import { Checkbox } from 'components/checkbox';
import CoreCharge from 'components/core-charge';
import Price from 'components/price';
import Tooltip from 'components/tooltip';
import { Money, ProductVariant } from 'lib/shopify/types';
import { useSearchParams } from 'next/navigation';
@ -20,32 +17,28 @@ const PriceWithCoreCharge = ({ variants, defaultPrice }: PriceWithCoreChargeProp
(option) => option.value === searchParams.get(option.name.toLowerCase())
)
);
console.log({ variant });
const price = variant?.price.amount || defaultPrice.amount;
return (
<>
<div className="mb-4">
{variant && (
<div className="flex flex-row items-center space-x-3 text-sm text-neutral-700">
{variant.sku && <span>SKU: {variant.sku}</span>}
{variant.barcode && <span>Part Number: {variant.barcode}</span>}
</div>
)}
</div>
<div className="mr-auto flex w-auto flex-row flex-wrap items-center gap-3 text-sm">
<Price
amount={variant?.price.amount || defaultPrice.amount}
amount={price}
currencyCode={variant?.price.currencyCode || defaultPrice.currencyCode}
className="text-lg font-semibold"
className="text-2xl font-semibold"
/>
<CoreCharge variant={variant} />
{variant?.coreCharge?.amount && variant.waiverAvailable ? (
<div className="mt-1 flex w-full items-center space-x-3">
<Checkbox id="payCoreCharge" />
<label htmlFor="payCoreCharge" className="text-md flex items-center gap-1 leading-none">
Pay a core charge of
<Price
amount={variant.coreCharge.amount}
currencyCode={variant.coreCharge.currencyCode}
/>
<span data-tooltip-id="payCoreCharge">
<InformationCircleIcon className="ml-1 h-4 w-4 text-gray-500" />
</span>
</label>
<Tooltip id="payCoreCharge">Select this if you do not have a core to return</Tooltip>
</div>
) : null}
</div>
</>
);
};

View File

@ -3,13 +3,15 @@ import Prose from 'components/prose';
import { Product } from 'lib/shopify/types';
import { Suspense } from 'react';
import PriceWithCoreCharge from './price-with-core-charge';
import SpecialOffer from './special-offer';
import { VariantSelector } from './variant-selector';
import Warranty from './warranty';
export function ProductDescription({ product }: { product: Product }) {
return (
<>
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
<h1 className="mb-3 text-4xl font-bold">{product.title}</h1>
<div className="mb-5 flex flex-col dark:border-neutral-700">
<h1 className="mb-3 text-2xl font-bold">{product.title}</h1>
<PriceWithCoreCharge
variants={product.variants}
defaultPrice={product.priceRange.minVariantPrice}
@ -21,14 +23,21 @@ export function ProductDescription({ product }: { product: Product }) {
{product.descriptionHtml ? (
<Prose
className="mb-6 text-sm leading-tight dark:text-white/[60%]"
className="mb-4 text-sm leading-tight dark:text-white/[60%]"
html={product.descriptionHtml}
/>
) : null}
<div className="mb-4 border-t py-6 dark:border-neutral-700">
<Warranty productType={product.productType} />
</div>
<Suspense fallback={null}>
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
</Suspense>
<div className="mt-4 border-t pt-4">
<SpecialOffer />
</div>
</>
);
}

View File

@ -0,0 +1,27 @@
import { CurrencyDollarIcon, ShieldCheckIcon, UsersIcon } from '@heroicons/react/24/outline';
import { TruckIcon } from '@heroicons/react/24/solid';
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 tracking-normal text-neutral-800">
<p className="flex items-center gap-3">
<TruckIcon className="h-5 w-5 text-secondary" /> Flat Rate Shipping (Commercial Address)
</p>
<p className="flex items-center gap-3">
<ShieldCheckIcon className="h-5 w-5 text-secondary" /> Up to 5 Years Unlimited Miles
Warranty
</p>
<p className="flex items-center gap-3">
<UsersIcon className="h-5 w-5 text-secondary" /> Excellent Customer Support
</p>
<p className="flex items-center gap-3">
<CurrencyDollarIcon className="h-5 w-5 text-secondary" /> No Core Charge for 30 days
</p>
</div>
</>
);
};
export default SpecialOffer;

View File

@ -39,8 +39,8 @@ export function VariantSelector({
}));
return options.map((option) => (
<dl className="mb-8" key={option.id}>
<dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt>
<dl className="mb-6" key={option.id}>
<dt className="mb-4 text-sm font-medium tracking-wide">{option.name}</dt>
<dd className="flex flex-wrap gap-3">
{option.values.map((value) => {
const optionNameLowerCase = option.name.toLowerCase();

View File

@ -0,0 +1,57 @@
'use client';
import Price from 'components/price';
import { cn } from 'lib/utils';
import { ReactNode, useState } from 'react';
const options = ['Included', 'Premium Labor', '+1 Year'] as const;
type Option = (typeof options)[number];
const plans: Array<{
key: Option;
template: ReactNode;
price: number;
}> = [
{
template: (
<>
<span>Included</span>
<span>3-Year Warranty</span>
</>
),
price: 0,
key: 'Included'
},
{
template: <span>Premium Labor</span>,
price: 150,
key: 'Premium Labor'
},
{
template: <span>+1 Year</span>,
price: 100,
key: '+1 Year'
}
];
const WarrantySelector = () => {
const [selectedOptions, setSelectedOptions] = useState<Option>('Included');
return (
<ul className="flex min-h-16 flex-row space-x-4 pt-2">
{plans.map((plan) => (
<li
key={plan.key}
onClick={() => setSelectedOptions(plan.key)}
className={cn(
'flex w-32 cursor-pointer flex-col items-center justify-center space-y-2 rounded-md border p-2 text-xs font-medium',
{ 'ring-2 ring-secondary': plan.key === selectedOptions }
)}
>
{plan.template}
<Price amount={String(plan.price)} currencyCode="USD" />
</li>
))}
</ul>
);
};
export default WarrantySelector;

View File

@ -0,0 +1,30 @@
import { ShieldCheckIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import WarrantySelector from './warranty-selector';
type WarrantyProps = {
productType: string | null;
};
const Warranty = ({ productType }: WarrantyProps) => {
return (
<div className="flex flex-col 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 {productType ?? 'product'}</span>
</div>
<div className="mb-1 flex flex-row items-center space-x-3 divide-x divide-gray-400 leading-none">
<span>Extended Warranty</span>
<Link href="#" className="pl-2 text-blue-800 hover:underline">
What&apos;s Included
</Link>
<Link href="#" className="pl-2 text-blue-800 hover:underline">
Terms & Conditions
</Link>
</div>
<WarrantySelector />
</div>
);
};
export default Warranty;

View File

@ -36,6 +36,8 @@ const productFragment = /* GraphQL */ `
id
title
availableForSale
barcode
sku
selectedOptions {
name
value
@ -68,6 +70,9 @@ const productFragment = /* GraphQL */ `
}
tags
updatedAt
productType: metafield(namespace: "custom", key: "product_type") {
value
}
}
${imageFragment}
${seoFragment}

View File

@ -193,12 +193,12 @@ const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean =
return undefined;
}
const { images, variants, ...rest } = product;
const { images, variants, productType, ...rest } = product;
return {
...rest,
images: reshapeImages(images, product.title),
variants: reshapeVariants(removeEdgesAndNodes(variants))
variants: reshapeVariants(removeEdgesAndNodes(variants)),
productType: productType?.value ?? null
};
};

View File

@ -62,9 +62,10 @@ export type Page = {
updatedAt: string;
};
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
export type Product = Omit<ShopifyProduct, 'variants' | 'images' | 'productType'> & {
variants: ProductVariant[];
images: Image[];
productType: string | null;
};
export type ProductOption = {
@ -84,6 +85,8 @@ export type ProductVariant = {
price: Money;
coreCharge: Money | null;
waiverAvailable: boolean | null;
barcode: string | null;
sku: string | null;
};
export type ShopifyProductVariant = Omit<ProductVariant, 'coreCharge' | 'waiverAvailable'> & {
@ -140,6 +143,9 @@ export type ShopifyProduct = {
handle: string;
}[];
};
productType: {
value: string;
} | null;
};
export type ShopifyCartOperation = {