feat: add metafields to variant options

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-05-03 12:28:37 +07:00
parent 2ad07c3682
commit e0cd6ac2bd
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
4 changed files with 152 additions and 122 deletions

View File

@ -127,139 +127,151 @@ export function VariantSelector({
<XMarkIcon className="h-6" /> <XMarkIcon className="h-6" />
</button> </button>
</div> </div>
<div className="mt-5 flex h-full flex-col justify-between overflow-hidden "> <div className="mt-5 flex h-full flex-col justify-between overflow-hidden">
<div> {options.map((option) => {
{options.map((option) => { return (
return ( <ul
<ul key={option.id}
key={option.id} className="flex-grow flex-col space-y-4 overflow-auto px-2 py-4"
className="flex-grow flex-col space-y-4 overflow-auto px-1 py-4" >
> {option.values.map((value) => {
{option.values.map((value) => { const optionNameLowerCase = option.name.toLowerCase();
const optionNameLowerCase = option.name.toLowerCase(); const optionSearchParams = new URLSearchParams(searchParams.toString());
const optionSearchParams = new URLSearchParams(searchParams.toString());
optionSearchParams.set(optionNameLowerCase, value); optionSearchParams.set(optionNameLowerCase, value);
// In order to determine if an option is available for sale, we need to: // In order to determine if an option is available for sale, we need to:
// //
// 1. Filter out all other param state // 1. Filter out all other param state
// 2. Filter out invalid options // 2. Filter out invalid options
// 3. Check if the option combination is available for sale // 3. Check if the option combination is available for sale
// //
// This is the "magic" that will cross check possible variant combinations and preemptively // This is the "magic" that will cross check possible variant combinations and preemptively
// disable combinations that are not available. For example, if the color gray is only available in size medium, // disable combinations that are not available. For example, if the color gray is only available in size medium,
// then all other sizes should be disabled. // then all other sizes should be disabled.
const filtered = Array.from(optionSearchParams.entries()).filter( const filtered = Array.from(optionSearchParams.entries()).filter(
([key, value]) => ([key, value]) =>
options.find( options.find(
(option) => (option) =>
option.name.toLowerCase() === key && option.values.includes(value) option.name.toLowerCase() === key && option.values.includes(value)
)
);
const isAvailableForSale = combinations.find((combination) =>
filtered.every(
([key, value]) =>
combination[key] === value && combination.availableForSale
) )
); );
const variant = isAvailableForSale const isAvailableForSale = combinations.find((combination) =>
? variantsById[isAvailableForSale.id] filtered.every(
: undefined; ([key, value]) =>
combination[key] === value && combination.availableForSale
)
);
const coreChargeOptions = [ const variant = isAvailableForSale
variant?.waiverAvailable && { ? variantsById[isAvailableForSale.id]
label: 'Core Waiver', : undefined;
value: CORE_WAIVER,
price: { amount: 0, currencyCode: variant?.price.currencyCode }
},
variant?.coreVariantId &&
variant.coreCharge && {
label: 'Core Charge',
value: variant.coreVariantId,
price: variant.coreCharge
}
].filter(Boolean) as CoreChargeOption[];
// preset the first core charge option if not set const coreChargeOptions = [
coreChargeOptions[0] && variant?.waiverAvailable && {
optionSearchParams.set(CORE_VARIANT_ID_KEY, coreChargeOptions[0].value); label: 'Core Waiver',
value: CORE_WAIVER,
price: { amount: 0, currencyCode: variant?.price.currencyCode }
},
variant?.coreVariantId &&
variant.coreCharge && {
label: 'Core Charge',
value: variant.coreVariantId,
price: variant.coreCharge
}
].filter(Boolean) as CoreChargeOption[];
const optionUrl = createUrl(pathname, optionSearchParams); // preset the first core charge option if not set
coreChargeOptions[0] &&
optionSearchParams.set(CORE_VARIANT_ID_KEY, coreChargeOptions[0].value);
// The option is active if it's in the url params. const optionUrl = createUrl(pathname, optionSearchParams);
const isActive = searchParams.get(optionNameLowerCase) === value;
return ( // The option is active if it's in the url params.
<li const isActive = searchParams.get(optionNameLowerCase) === value;
key={value}
className={clsx('flex w-full rounded border border-neutral-300', { return (
'cursor-default ring-2 ring-secondary': isActive, <li
'ring-2 ring-transparent hover:ring-secondary': key={value}
!isActive && isAvailableForSale, className={clsx('flex w-full rounded border border-neutral-300', {
'cursor-not-allowed opacity-60 ring-1 ring-neutral-300': 'cursor-default ring-2 ring-secondary': isActive,
!isAvailableForSale 'ring-2 ring-transparent hover:ring-secondary':
})} !isActive && isAvailableForSale,
'cursor-not-allowed opacity-60 ring-1 ring-neutral-300':
!isAvailableForSale
})}
>
<button
disabled={!isAvailableForSale}
aria-disabled={!isAvailableForSale}
onClick={() => router.replace(optionUrl, { scroll: false })}
className="flex w-full flex-col gap-2 px-4 py-3"
> >
<button <div className="flex w-full flex-row items-center justify-between">
disabled={!isAvailableForSale} <div className="flex flex-col items-start gap-1">
aria-disabled={!isAvailableForSale} {variant ? (
onClick={() => router.replace(optionUrl, { scroll: false })} <Price
className="flex w-full flex-col gap-2 px-4 py-3" amount={variant.price.amount}
> currencyCode={variant.price.currencyCode}
<div className="flex w-full flex-row items-center justify-between"> className="text-base font-semibold"
<div className="flex flex-col items-start gap-1"> />
{variant ? ( ) : null}
<Price <div className="flex items-center gap-1">
amount={variant.price.amount} <span className="text-xs font-medium text-gray-600">
currencyCode={variant.price.currencyCode} {option.name}:
className="text-base font-semibold" </span>
/> <span className="text-xs text-gray-600">{value}</span>
) : null}
<div className="flex items-center gap-1">
<span className="text-xs font-medium text-gray-600">
{option.name}:
</span>
<span className="text-xs text-gray-600">{value}</span>
</div>
</div> </div>
{!isAvailableForSale ? <span>Out of Stock</span> : null}
</div> </div>
<div className="mt-1.5 flex flex-row flex-wrap items-center gap-3"> {!isAvailableForSale ? <span>Out of Stock</span> : null}
{coreChargeOptions.map((option) => ( </div>
<button <div className="mt-1.5 flex flex-row flex-wrap items-center gap-3">
key={option.value} {coreChargeOptions.map((option) => (
disabled={!isActive} <button
className={clsx( key={option.value}
'flex flex-row items-center gap-2 rounded-full border border-neutral-300 px-3 py-1 text-xs', disabled={!isActive}
{ className={clsx(
'bg-gray-200': 'flex flex-row items-center gap-2 rounded-full border border-neutral-300 px-3 py-1 text-xs',
isActive && {
searchParams.get(CORE_VARIANT_ID_KEY) === option.value, 'bg-gray-200':
'bg-transparent': isActive &&
searchParams.get(CORE_VARIANT_ID_KEY) !== option.value, searchParams.get(CORE_VARIANT_ID_KEY) === option.value,
'cursor-not-allowed opacity-50 hover:bg-transparent': 'bg-transparent':
!isActive, searchParams.get(CORE_VARIANT_ID_KEY) !== option.value,
'hover:bg-gray-200': isActive 'cursor-not-allowed opacity-50 hover:bg-transparent':
} !isActive,
)} 'hover:bg-gray-200': isActive
onClick={(e) => handleCoreChargeClick(e, option.value)} }
> )}
<span>{option.label}</span> onClick={(e) => handleCoreChargeClick(e, option.value)}
<Price {...option.price} /> >
</button> <span>{option.label}</span>
))} <Price {...option.price} />
</button>
))}
</div>
<div className="mt-2 flex w-full flex-col gap-1 border-t border-gray-300 pl-1 pt-2 text-xs tracking-normal">
<div className="flex flex-row items-center gap-2">
<span>Condition:</span>
<span>{variant?.condition || 'N/A'}</span>
</div> </div>
</button> <div className="flex flex-row items-center gap-2">
</li> <span>Estimated Delivery:</span>
); <span>{variant?.estimatedDelivery || 'N/A'}</span>
})} </div>
</ul> <div className="flex flex-row items-center gap-2">
); <span>Mileage:</span>
})} <span>{variant?.mileage || 'N/A'}</span>
</div> </div>
</div>
</button>
</li>
);
})}
</ul>
);
})}
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>

View File

@ -55,6 +55,15 @@ const productFragment = /* GraphQL */ `
coreVariantId: metafield(namespace: "custom", key: "coreVariant") { coreVariantId: metafield(namespace: "custom", key: "coreVariant") {
value value
} }
estimatedDelivery: metafield(namespace: "custom", key: "delivery") {
value
}
mileage: metafield(namespace: "custom", key: "mileage") {
value
}
condition: metafield(namespace: "custom", key: "condition") {
value
}
} }
} }
} }

View File

@ -185,7 +185,10 @@ const reshapeVariants = (variants: ShopifyProductVariant[]): ProductVariant[] =>
...variant, ...variant,
waiverAvailable: parseMetaFieldValue<boolean>(variant.waiverAvailable), waiverAvailable: parseMetaFieldValue<boolean>(variant.waiverAvailable),
coreVariantId: variant.coreVariantId?.value || null, coreVariantId: variant.coreVariantId?.value || null,
coreCharge: parseMetaFieldValue<Money>(variant.coreCharge) coreCharge: parseMetaFieldValue<Money>(variant.coreCharge),
mileage: variant.mileage?.value ?? null,
estimatedDelivery: variant.estimatedDelivery?.value || null,
condition: variant.condition?.value || null
})); }));
}; };

View File

@ -87,15 +87,21 @@ export type ProductVariant = {
barcode: string | null; barcode: string | null;
sku: string | null; sku: string | null;
coreVariantId: string | null; coreVariantId: string | null;
mileage: number | null;
estimatedDelivery: string | null;
condition: string | null;
}; };
export type ShopifyProductVariant = Omit< export type ShopifyProductVariant = Omit<
ProductVariant, ProductVariant,
'coreCharge' | 'waiverAvailable' | 'coreVariantId' 'coreCharge' | 'waiverAvailable' | 'coreVariantId' | 'mileage' | 'estimatedDelivery' | 'condition'
> & { > & {
waiverAvailable: { value: string }; waiverAvailable: { value: string };
coreVariantId: { value: string } | null; coreVariantId: { value: string } | null;
coreCharge: { value: string } | null; coreCharge: { value: string } | null;
mileage: { value: number } | null;
estimatedDelivery: { value: string } | null;
condition: { value: string } | null;
}; };
export type SEO = { export type SEO = {