fix: update core charge appearance

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-04-26 19:24:58 +07:00
parent 3a3ff3798f
commit 3bf7fa5af9
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
14 changed files with 196 additions and 146 deletions

View File

@ -10,10 +10,10 @@ import { useFormState, useFormStatus } from 'react-dom';
function SubmitButton({
availableForSale,
selectedVariantId
disabled
}: {
availableForSale: boolean;
selectedVariantId: string | undefined;
disabled: boolean;
}) {
const { pending } = useFormStatus();
const buttonClasses =
@ -28,7 +28,7 @@ function SubmitButton({
);
}
if (!selectedVariantId) {
if (disabled) {
return (
<button
aria-label="Please select an option"
@ -75,11 +75,15 @@ export function AddToCart({
)
);
const selectedVariantId = variant?.id || defaultVariantId;
const missingCoreVariantId = variant?.coreVariantId && !searchParams.has('coreVariantId');
const actionWithVariant = formAction.bind(null, selectedVariantId);
return (
<form action={actionWithVariant}>
<SubmitButton availableForSale={availableForSale} selectedVariantId={selectedVariantId} />
<SubmitButton
availableForSale={availableForSale}
disabled={Boolean(!selectedVariantId || missingCoreVariantId)}
/>
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>

View File

@ -1,21 +0,0 @@
import CoreCharge from 'components/core-charge';
import { ProductVariant } from 'lib/shopify/types';
type CoreChargeBadgeProps = {
selectedOptions: {
name: string;
value: string;
}[];
variants: ProductVariant[];
};
const CoreChargeBadge = ({ variants, selectedOptions }: CoreChargeBadgeProps) => {
const selectedOptionsMap = new Map(selectedOptions.map((option) => [option.name, option.value]));
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every((option) => option.value === selectedOptionsMap.get(option.name))
);
return <CoreCharge variant={variant} sm />;
};
export default CoreChargeBadge;

View File

@ -10,7 +10,6 @@ import Image from 'next/image';
import Link from 'next/link';
import { Fragment, useEffect, useRef, useState } from 'react';
import CloseCart from './close-cart';
import CoreChargeBadge from './core-charge-badge';
import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button';
import OpenCart from './open-cart';
@ -136,17 +135,11 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
</Link>
</div>
<div className="ml-20 flex flex-col gap-2">
<div className="flex flex-row items-center gap-2">
<Price
className="font-semibold"
amount={item.cost.totalAmount.amount}
currencyCode={item.cost.totalAmount.currencyCode}
/>
<CoreChargeBadge
variants={item.merchandise.product.variants}
selectedOptions={item.merchandise.selectedOptions}
/>
</div>
<Price
className="font-semibold"
amount={item.cost.totalAmount.amount}
currencyCode={item.cost.totalAmount.currencyCode}
/>
<div className="flex h-9 w-fit flex-row items-center rounded-sm border border-neutral-300 dark:border-neutral-700">
<EditItemQuantityButton item={item} type="minus" />
<p className="w-6 text-center">

View File

@ -1,48 +0,0 @@
import { ArrowPathIcon } from '@heroicons/react/16/solid';
import { ProductVariant } from 'lib/shopify/types';
import Price from './price';
import Tooltip from './tooltip';
type CoreChargeProps = {
variant?: ProductVariant;
sm?: boolean;
};
const CoreCharge = ({ variant, sm = false }: CoreChargeProps) => {
if (!variant || !variant.coreCharge?.amount || variant.waiverAvailable) return null;
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-3 w-3" />
<span
className="flex items-center gap-1"
data-tooltip-id={!sm ? 'core-charge-explanation' : undefined}
>
{sm ? coreChargeDisplay : <>Core Charge: {coreChargeDisplay}</>}
</span>
<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 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 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>
);
};
export default CoreCharge;

View File

@ -0,0 +1,103 @@
'use client';
import Price from 'components/price';
import { CORE_VARIANT_ID_KEY, CORE_WAIVER } from 'lib/constants';
import { Money, ProductVariant } from 'lib/shopify/types';
import { cn, createUrl } from 'lib/utils';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
type CoreChargeProps = {
variants: ProductVariant[];
defaultPrice: Money;
};
const CoreCharge = ({ variants, defaultPrice }: CoreChargeProps) => {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const optionSearchParams = new URLSearchParams(searchParams);
const coreVariantIdSearchParam = optionSearchParams.get(CORE_VARIANT_ID_KEY);
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every(
(option) => option.value === optionSearchParams.get(option.name.toLowerCase())
)
);
const { coreCharge, waiverAvailable } = variant ?? {};
const handleSelectCoreChargeOption = (action: 'add' | 'remove') => {
if (action === 'add' && variant?.coreVariantId) {
optionSearchParams.set(CORE_VARIANT_ID_KEY, variant.coreVariantId);
} else if (action === 'remove') {
optionSearchParams.set(CORE_VARIANT_ID_KEY, CORE_WAIVER);
}
const newUrl = createUrl(pathname, optionSearchParams);
router.replace(newUrl, { scroll: false });
};
// if the selected variant has changed, and the core change variant id is not the same as the selected variant id
// or if users have selected the core waiver but the selected variant does not have a waiver available
// we remove the core charge from the url
if (
variant?.coreVariantId &&
optionSearchParams.has(CORE_VARIANT_ID_KEY) &&
(coreVariantIdSearchParam !== CORE_WAIVER || !variant.waiverAvailable) &&
coreVariantIdSearchParam !== variant.coreVariantId
) {
optionSearchParams.delete(CORE_VARIANT_ID_KEY);
const newUrl = createUrl(pathname, optionSearchParams);
router.replace(newUrl, { scroll: false });
}
const selectedPayCoreCharge = coreVariantIdSearchParam === variant?.coreVariantId;
const selectedCoreWaiver = coreVariantIdSearchParam === CORE_WAIVER;
return (
<div className="flex flex-col text-xs lg:text-sm">
<div className="mb-2 text-base font-medium">Core Charge</div>
<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&apos;ll receive a refund of the core charge.
</p>
<ul className="flex min-h-16 flex-row space-x-4 pt-2">
{waiverAvailable ? (
<li className="flex w-32">
<button
onClick={() => handleSelectCoreChargeOption('remove')}
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',
{
'ring-2 ring-secondary': selectedCoreWaiver
}
)}
>
<span>Core Waiver</span>
<Price amount="0" currencyCode={defaultPrice.currencyCode} />
</button>
</li>
) : null}
{coreCharge && variant?.coreVariantId ? (
<li className="flex w-32">
<button
onClick={() => handleSelectCoreChargeOption('add')}
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',
{
'ring-2 ring-secondary': selectedPayCoreCharge
}
)}
>
<span>Core Charge</span>
<Price amount={coreCharge.amount} currencyCode={coreCharge.currencyCode} />
</button>
</li>
) : null}
</ul>
</div>
);
};
export default CoreCharge;

View File

@ -1,45 +0,0 @@
'use client';
import CoreCharge from 'components/core-charge';
import Price from 'components/price';
import { Money, ProductVariant } from 'lib/shopify/types';
import { useSearchParams } from 'next/navigation';
type PriceWithCoreChargeProps = {
variants: ProductVariant[];
defaultPrice: Money;
};
const PriceWithCoreCharge = ({ variants, defaultPrice }: PriceWithCoreChargeProps) => {
const searchParams = useSearchParams();
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every(
(option) => option.value === searchParams.get(option.name.toLowerCase())
)
);
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={price}
currencyCode={variant?.price.currencyCode || defaultPrice.currencyCode}
className="text-2xl font-semibold"
/>
<CoreCharge variant={variant} />
</div>
</>
);
};
export default PriceWithCoreCharge;

View File

@ -2,8 +2,9 @@ import { AddToCart } from 'components/cart/add-to-cart';
import Prose from 'components/prose';
import { Product } from 'lib/shopify/types';
import { Suspense } from 'react';
import PriceWithCoreCharge from './price-with-core-charge';
import CoreCharge from './core-charge';
import SpecialOffer from './special-offer';
import VariantPrice from './vairant-price';
import { VariantSelector } from './variant-selector';
import Warranty from './warranty';
@ -12,7 +13,7 @@ export function ProductDescription({ product }: { product: Product }) {
<>
<div className="mb-5 flex flex-col dark:border-neutral-700">
<h1 className="mb-3 text-2xl font-bold">{product.title}</h1>
<PriceWithCoreCharge
<VariantPrice
variants={product.variants}
defaultPrice={product.priceRange.minVariantPrice}
/>
@ -28,6 +29,10 @@ export function ProductDescription({ product }: { product: Product }) {
/>
) : null}
<div className="mb-4 border-t pb-4 pt-6 dark:border-neutral-700">
<CoreCharge variants={product.variants} defaultPrice={product.priceRange.minVariantPrice} />
</div>
<div className="mb-4 border-t py-6 dark:border-neutral-700">
<Warranty productType={product.productType} />
</div>

View File

@ -0,0 +1,31 @@
'use client';
import Price from 'components/price';
import { Money, ProductVariant } from 'lib/shopify/types';
import { useSearchParams } from 'next/navigation';
type PriceWithCoreChargeProps = {
variants: ProductVariant[];
defaultPrice: Money;
};
const VariantPrice = ({ variants, defaultPrice }: PriceWithCoreChargeProps) => {
const searchParams = useSearchParams();
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every(
(option) => option.value === searchParams.get(option.name.toLowerCase())
)
);
const price = variant?.price.amount || defaultPrice.amount;
return (
<Price
amount={price}
currencyCode={variant?.price.currencyCode || defaultPrice.currencyCode}
className="text-2xl font-semibold"
/>
);
};
export default VariantPrice;

View File

@ -51,6 +51,7 @@ export function VariantSelector({
// Update the option params using the current option to reflect how the url *would* change,
// if the option was clicked.
optionSearchParams.set(optionNameLowerCase, value);
const optionUrl = createUrl(pathname, optionSearchParams);
// In order to determine if an option is available for sale, we need to:

View File

@ -38,16 +38,19 @@ const WarrantySelector = () => {
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-center text-xs font-medium',
{ 'ring-2 ring-secondary': plan.key === selectedOptions }
)}
>
{plan.template}
<Price amount={String(plan.price)} currencyCode="USD" />
<li key={plan.key} className="flex w-32">
<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',
{
'ring-2 ring-secondary': plan.key === selectedOptions
}
)}
>
{plan.template}
<Price amount={String(plan.price)} currencyCode="USD" />
</button>
</li>
))}
</ul>

View File

@ -29,3 +29,6 @@ export const TAGS = {
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
export const DEFAULT_OPTION = 'Default Title';
export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2024-04/graphql.json';
export const CORE_WAIVER = 'core-waiver';
export const CORE_VARIANT_ID_KEY = 'coreVariantId';

View File

@ -52,6 +52,9 @@ const productFragment = /* GraphQL */ `
waiverAvailable: metafield(namespace: "custom", key: "waiver_available") {
value
}
coreVariantId: metafield(namespace: "custom", key: "coreVariant") {
value
}
}
}
}

View File

@ -183,8 +183,9 @@ const reshapeImages = (images: Connection<Image>, productTitle: string) => {
const reshapeVariants = (variants: ShopifyProductVariant[]): ProductVariant[] => {
return variants.map((variant) => ({
...variant,
coreCharge: parseMetaFieldValue<Money>(variant.coreCharge),
waiverAvailable: parseMetaFieldValue<boolean>(variant.waiverAvailable)
waiverAvailable: parseMetaFieldValue<boolean>(variant.waiverAvailable),
coreVariantId: variant.coreVariantId?.value || null,
coreCharge: parseMetaFieldValue<Money>(variant.coreCharge)
}));
};
@ -404,6 +405,18 @@ export async function getProduct(handle: string): Promise<Product | undefined> {
return reshapeProduct(res.body.data.product, false);
}
export async function getProductVariant(handle: string): Promise<Product | undefined> {
const res = await shopifyFetch<ShopifyProductOperation>({
query: getProductQuery,
tags: [TAGS.products],
variables: {
handle
}
});
return reshapeProduct(res.body.data.product, false);
}
export async function getProductRecommendations(productId: string): Promise<Product[]> {
const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
query: getProductRecommendationsQuery,

View File

@ -87,11 +87,16 @@ export type ProductVariant = {
waiverAvailable: boolean | null;
barcode: string | null;
sku: string | null;
coreVariantId: string | null;
};
export type ShopifyProductVariant = Omit<ProductVariant, 'coreCharge' | 'waiverAvailable'> & {
coreCharge: { value: string } | null;
export type ShopifyProductVariant = Omit<
ProductVariant,
'coreCharge' | 'waiverAvailable' | 'coreVariantId'
> & {
waiverAvailable: { value: string };
coreVariantId: { value: string } | null;
coreCharge: { value: string } | null;
};
export type SEO = {