Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ export const MarketplaceLanding = () => {
updateSearchParam('query', searchString || undefined);
};

// Filter products here based on search and type filters. If no filters are set, shows all available products.
// Filter products here based on category, search and type filters. If no filters are set, shows all available products.
const filteredProducts = React.useMemo(
() => filterProducts(PRODUCTS, { searchQuery, selectedType }),
[searchQuery, selectedType]
() =>
filterProducts(PRODUCTS, { searchQuery, selectedCategory, selectedType }),
[searchQuery, selectedCategory, selectedType]
);

// Group filtered products by category
Expand All @@ -107,22 +108,37 @@ export const MarketplaceLanding = () => {
return map;
}, [filteredProducts]);

// Get categories that have at least one filtered product
const categoriesWithFilteredProducts = React.useMemo(
() => Object.keys(filteredProductsByCategory) as Category[],
[filteredProductsByCategory]
);

// Filter categories based on:
// 1. Selected category from dropdown (if set)
// 2. All categories, sorted by product count (if no filters)
// 2. All categories that have filtered products, sorted by product count (if no category selected)
const filteredCategories = React.useMemo(() => {
if (selectedCategory) {
return categoriesWithProducts.filter((cat) => cat === selectedCategory);
return categoriesWithFilteredProducts.filter(
(cat) => cat === selectedCategory
);
}
// No filters - show all categories, sorted by product count (highest to lowest)
return [...categoriesWithProducts].sort((a, b) => {

// Show all categories sorted by product count (highest to lowest)
return [...categoriesWithFilteredProducts].sort((a, b) => {
const countA = filteredProductsByCategory[a]?.length || 0;
const countB = filteredProductsByCategory[b]?.length || 0;
return countB - countA;
});
}, [selectedCategory, categoriesWithProducts, filteredProductsByCategory]);
}, [
selectedCategory,
categoriesWithFilteredProducts,
filteredProductsByCategory,
]);

const hasFiltersApplied = Boolean(searchQuery || selectedType);
const hasFiltersApplied = Boolean(
searchQuery || selectedCategory || selectedType
);

// Show empty state if there are no products to display (either no products exist, or filters return no results)
const showEmptyState = filteredProducts.length === 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,65 @@ describe('filterProducts', () => {
const filtered = filterProducts(products, { searchQuery: 'randomtext' });
expect(filtered).toHaveLength(0);
});

it('filters by category', () => {
const filtered = filterProducts(products, {
selectedCategory: 'Kubernetes',
});
expect(filtered).toHaveLength(1);
expect(filtered[0].name).toBe('SpinKube');
});

it('filters by category and search query: category first, then search', () => {
const filtered = filterProducts(products, {
selectedCategory: 'Kubernetes',
searchQuery: 'spin',
});
expect(filtered).toHaveLength(1);
expect(filtered[0].name).toBe('SpinKube');
});

it('filters by category and search query: returns empty when search does not match category products', () => {
const filtered = filterProducts(products, {
selectedCategory: 'Kubernetes',
searchQuery: 'titan', // TITAN-Edge is not in Kubernetes category
});
expect(filtered).toHaveLength(0);
});

it('filters by category and type: category first, then type', () => {
const filtered = filterProducts(products, {
selectedCategory: 'Development Tools',
selectedType: 'SaaS & APIs',
});
expect(filtered).toHaveLength(1);
expect(filtered[0].name).toBe('APImetrics');
});

it('filters by category and type: returns empty when type does not match category products', () => {
const filtered = filterProducts(products, {
selectedCategory: 'Development Tools',
selectedType: 'Virtual Machines', // No VM products in Development Tools
});
expect(filtered).toHaveLength(0);
});

it('filters by all three filters: category, type, and search', () => {
const filtered = filterProducts(products, {
selectedCategory: 'Kubernetes',
selectedType: 'SaaS & APIs',
searchQuery: 'kube',
});
expect(filtered).toHaveLength(1);
expect(filtered[0].name).toBe('SpinKube');
});

it('filters by all three filters: returns empty when all filters applied but no match', () => {
const filtered = filterProducts(products, {
selectedCategory: 'Kubernetes',
selectedType: 'SaaS & APIs',
searchQuery: 'titan', // TITAN-Edge is not in Kubernetes
});
expect(filtered).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import type { Product } from '../shared';
import type { Category, Product } from '../shared';

/**
* Filters the given list of products by type and/or search query.
* Filters the given list of products by category, type and/or search query.
*
* - If no filters are provided, returns all products.
* - If a category is provided, only products that have that category are included (applied first).
* - If a type is provided, only products matching that type are included.
* - If a search query is provided, only products whose name, short description,
* partner name, or type name include the query (case-insensitive) are included.
*
* @param products The list of products to filter.
* @param filters An object containing optional searchQuery and selectedType.
* @param filters An object containing optional selectedCategory, searchQuery and selectedType.
*/
export const filterProducts = (
products: Product[],
filters: { searchQuery?: string; selectedType?: string }
filters: {
searchQuery?: string;
selectedCategory?: string;
selectedType?: string;
}
): Product[] => {
let result = products;
if (filters.selectedCategory) {
// Apply category filter first if present
result = result.filter((p) =>
p.categories.includes(filters.selectedCategory as Category)
);
}
if (filters.selectedType) {
result = result.filter((p) => p.type.name === filters.selectedType);
}
Expand Down