© {copyrightDate} {copyrightName}
{copyrightName.length && !copyrightName.endsWith('.') ? '.' : ''} All rights reserved.
diff --git a/components/layout/search/filter/item.tsx b/components/layout/search/filter/item.tsx
index ff44627f1..4788bc51c 100644
--- a/components/layout/search/filter/item.tsx
+++ b/components/layout/search/filter/item.tsx
@@ -5,22 +5,17 @@ import { SortFilterItem } from 'lib/constants';
import { createUrl } from 'lib/utils';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
-import { useEffect, useState } from 'react';
import type { ListItem, PathFilterItem } from '.';
function PathFilterItem({ item }: { item: PathFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
- const [active, setActive] = useState(pathname === item.path);
+ const active = pathname === item.path;
const newParams = new URLSearchParams(searchParams.toString());
const DynamicTag = active ? 'p' : Link;
newParams.delete('q');
- useEffect(() => {
- setActive(pathname === item.path);
- }, [pathname, item.path]);
-
return (
{
- setActive(searchParams.get('sort') === item.slug);
- }, [searchParams, item.slug]);
-
return (
- options.find((option) => option.name.toLowerCase() === key && option.values.includes(value))
+ const combinations: Combination[] = variants.map((variant) => ({
+ id: variant.id,
+ availableForSale: variant.availableForSale,
+ // Adds key / value pairs for each variant (ie. "color": "Black" and "size": 'M").
+ ...variant.selectedOptions.reduce(
+ (accumulator, option) => ({ ...accumulator, [option.name.toLowerCase()]: option.value }),
+ {}
)
- );
-
- // Optimize variants for easier lookups.
- const optimizedVariants: OptimizedVariant[] = variants.map((variant) => {
- const optimized: OptimizedVariant = {
- id: variant.id,
- availableForSale: variant.availableForSale,
- params: new URLSearchParams()
- };
-
- variant.selectedOptions.forEach((selectedOption) => {
- const name = selectedOption.name.toLowerCase();
- const value = selectedOption.value;
-
- optimized[name] = value;
- optimized.params.set(name, value);
- });
-
- return optimized;
- });
-
- // Find the first variant that is:
- //
- // 1. Available for sale
- // 2. Matches all options specified in the url (note that this
- // could be a partial match if some options are missing from the url).
- //
- // If no match (full or partial) is found, use the first variant that is
- // available for sale.
- const selectedVariant: OptimizedVariant | undefined =
- optimizedVariants.find(
- (variant) =>
- variant.availableForSale &&
- Object.entries(paramsMap).every(([key, value]) => variant[key] === value)
- ) || optimizedVariants.find((variant) => variant.availableForSale);
-
- const selectedVariantParams = new URLSearchParams(selectedVariant?.params);
- const currentUrl = createUrl(pathname, currentParams);
- const selectedVariantUrl = createUrl(pathname, selectedVariantParams);
-
- if (currentUrl !== selectedVariantUrl) {
- router.replace(selectedVariantUrl);
- }
+ }));
return options.map((option) => (
- {option.name}
-
{option.values.map((value) => {
- // Base option params on selected variant params.
- const optionParams = new URLSearchParams(selectedVariantParams);
- // Update the params using the current option to reflect how the url would change.
- optionParams.set(option.name.toLowerCase(), value);
+ const optionNameLowerCase = option.name.toLowerCase();
- const optionUrl = createUrl(pathname, optionParams);
+ // Base option params on current params so we can preserve any other param state in the url.
+ const optionSearchParams = new URLSearchParams(searchParams.toString());
- // The option is active if it in the url params.
- const isActive = selectedVariantParams.get(option.name.toLowerCase()) === value;
+ // 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);
- // The option is available for sale if it fully matches the variant in the option's url params.
- // It's super important to note that this is the options params, *not* the selected variant's params.
- // This is the "magic" that will cross check possible future variant combinations and preemptively
- // disable combinations that are not possible.
- const isAvailableForSale = optimizedVariants.find((a) =>
- Array.from(optionParams.entries()).every(([key, value]) => a[key] === value)
- )?.availableForSale;
+ // In order to determine if an option is available for sale, we need to:
+ //
+ // 1. Filter out all other param state
+ // 2. Filter out invalid options
+ // 3. Check if the option combination is available for sale
+ //
+ // 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,
+ // then all other sizes should be disabled.
+ const filtered = Array.from(optionSearchParams.entries()).filter(([key, value]) =>
+ options.find(
+ (option) => option.name.toLowerCase() === key && option.values.includes(value)
+ )
+ );
+ const isAvailableForSale = combinations.find((combination) =>
+ filtered.every(
+ ([key, value]) => combination[key] === value && combination.availableForSale
+ )
+ );
+ // The option is active if it's in the url params.
+ const isActive = searchParams.get(optionNameLowerCase) === value;
+
+ // You can't disable a link, so we need to render something that isn't clickable.
const DynamicTag = isAvailableForSale ? Link : 'p';
const dynamicProps = {
...(isAvailableForSale && { scroll: false })
diff --git a/lib/shopware/api-extended.ts b/lib/shopware/api-extended.ts
index 99b67c1d7..87db588fa 100644
--- a/lib/shopware/api-extended.ts
+++ b/lib/shopware/api-extended.ts
@@ -10,6 +10,7 @@ type operationsWithoutOriginal = Omit<
| 'readProductCrossSellings'
| 'readProductListing'
| 'searchPage'
+ | 'addLineItem'
| 'readCart'
| 'deleteLineItem'
>;
@@ -21,6 +22,7 @@ export type extendedPaths =
| 'readProductCrossSellings post /product/{productId}/cross-selling'
| 'readProductListing post /product-listing/{categoryId}'
| 'searchPage post /search'
+ | 'addLineItem post /checkout/cart/line-item'
| 'readCart get /checkout/cart?name'
| 'deleteLineItem delete /checkout/cart/line-item?id[]={ids}'
| operationPaths;
@@ -32,6 +34,7 @@ export type extendedOperations = operationsWithoutOriginal & {
readProductCrossSellings: extendedReadProductCrossSellings;
readProductListing: extendedReadProductListing;
searchPage: extendedSearchPage;
+ addLineItem: extendedAddLineItem;
readCart: extendedReadCart;
deleteLineItem: extendedDeleteLineItem;
};
@@ -340,6 +343,26 @@ type extendedReadProductListing = {
};
};
+type extendedCartItems = components['schemas']['ArrayStruct'] & {
+ items?: Partial[];
+};
+
+type extendedAddLineItem = {
+ requestBody?: {
+ content: {
+ 'application/json': extendedCartItems;
+ };
+ };
+ responses: {
+ /** The updated cart. */
+ 200: {
+ content: {
+ 'application/json': ExtendedCart;
+ };
+ };
+ };
+};
+
type extendedReadCart = {
parameters: {
query?: {