mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 20:57:51 +00:00
feat: add metafields to variant options
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
2ad07c3682
commit
e0cd6ac2bd
@ -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>
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user