diff --git a/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx b/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx index 00e14ac11d..83e142320b 100644 --- a/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx +++ b/packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx @@ -406,6 +406,39 @@ export const AllResultsNoDate: Story = { }, } +export const AllResultsSameCategory: Story = { + name: "Should show category filter even if all items have same category", + args: generateArgs({ + collectionItems: COLLECTION_ITEMS.map((item) => ({ + ...item, + category: "The only categ0ry", + })), + }), + play: async ({ canvasElement }) => { + const screen = within(canvasElement) + const categoryFilter = screen.queryByText(/Category/i) + await expect(categoryFilter).toBeInTheDocument() + + const categoryItems = await screen.findAllByText(/The only categ0ry \(30\)/) + await expect(categoryItems.length).toBe(1) + }, +} + +export const AllResultsSameYear: Story = { + name: "Should show year filter if all items have same year", + args: generateArgs({ + collectionItems: COLLECTION_ITEMS.map((item) => ({ + ...item, + date: "2026-05-07", + })), + }), + play: async ({ canvasElement }) => { + const screen = within(canvasElement) + const yearFilter = screen.queryByText(/Year/i) + await expect(yearFilter).toBeInTheDocument() + }, +} + export const FileCard: Story = { args: generateArgs({ collectionItems: [COLLECTION_ITEMS[1]] as IsomerSitemap[], diff --git a/packages/components/src/templates/next/layouts/Collection/Collection.tsx b/packages/components/src/templates/next/layouts/Collection/Collection.tsx index 75f85633b7..cce6a1ab1c 100644 --- a/packages/components/src/templates/next/layouts/Collection/Collection.tsx +++ b/packages/components/src/templates/next/layouts/Collection/Collection.tsx @@ -14,6 +14,8 @@ import { Skeleton } from "../Skeleton" import CollectionClient from "./CollectionClient" import { getAvailableFilters, shouldShowDate } from "./utils" +const CATEGORY_OTHERS = "Others" + const getCollectionItems = ( site: IsomerSiteProps, permalink: string, @@ -62,7 +64,7 @@ const getCollectionItems = ( type: "collectionCard" as const, rawDate: date, lastUpdated: date?.toISOString(), - category: item.category || "Others", + category: item.category || CATEGORY_OTHERS, title: item.title, description: item.summary, image: item.image, diff --git a/packages/components/src/templates/next/layouts/Collection/utils.ts b/packages/components/src/templates/next/layouts/Collection/utils.ts deleted file mode 100644 index 150f03329c..0000000000 --- a/packages/components/src/templates/next/layouts/Collection/utils.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { - AppliedFilter, - FilterItem, - Filter as FilterType, -} from "../../types/Filter" -import type { ProcessedCollectionCardProps } from "~/interfaces" -import { getParsedDate } from "~/utils" - -const FILTER_ID_CATEGORY = "category" -const FILTER_ID_YEAR = "year" -const NO_SPECIFIED_YEAR_FILTER_ID = "not_specified" - -const getCategories = ( - tagCategories: Record>, -): FilterType[] => { - return Object.entries(tagCategories).reduce((acc: FilterType[], curValue) => { - const [category, values] = curValue - const items: FilterItem[] = Object.entries(values).map( - ([label, count]) => ({ - label, - count, - id: label, - }), - ) - - const filters: FilterType[] = [ - ...acc, - { - items, - id: category, - label: category, - }, - ] - - return filters - }, []) -} - -export const getAvailableFilters = ( - items: ProcessedCollectionCardProps[], -): FilterType[] => { - const categories: Record = {} - const years: Record = {} - // NOTE: Each tag is a mapping of a category to its - // associated set of values as well as the selected value. - // Hence, we store a map here of the category (eg: Body parts) - // to the number of occurences of each value (eg: { Brain: 3, Leg: 2}) - const tagCategories: Record> = {} - - let numberOfUndefinedDates = 0 - - items.forEach(({ category, lastUpdated, tags }) => { - // Step 1: Get all available categories - if (category in categories && categories[category]) { - categories[category] += 1 - } else { - categories[category] = 1 - } - - // Step 2: Get all available years - if (lastUpdated) { - const year = getParsedDate(lastUpdated).getFullYear().toString() - if (year in years && years[year]) { - years[year] += 1 - } else { - years[year] = 1 - } - } else { - numberOfUndefinedDates += 1 - } - - // Step 3: Get all category tags - if (tags) { - tags.forEach(({ selected: selectedLabels, category }) => { - selectedLabels.forEach((label) => { - if (!tagCategories[category]) { - tagCategories[category] = {} - } - if (!tagCategories[category][label]) { - tagCategories[category][label] = 0 - } - - tagCategories[category][label] += 1 - }) - }) - } - }) - - const yearFilterItems = Object.entries(years) - .map(([label, count]) => ({ - id: label.toLowerCase(), - label, - count, - })) - .sort((a, b) => parseInt(b.label) - parseInt(a.label)) - - const availableFilters: FilterType[] = [ - { - id: FILTER_ID_CATEGORY, - label: "Category", - items: Object.entries(categories) - .map(([label, count]) => ({ - id: label.toLowerCase(), - label: label.charAt(0).toUpperCase() + label.slice(1), - count, - })) - .sort((a, b) => a.label.localeCompare(b.label)), - }, - { - id: FILTER_ID_YEAR, - label: "Year", - // do not show "not specified" option if all items have undefined dates - items: - yearFilterItems.length === 0 - ? [] - : numberOfUndefinedDates === 0 - ? yearFilterItems - : [ - ...yearFilterItems, - { - id: NO_SPECIFIED_YEAR_FILTER_ID, - label: "Not specified", - count: numberOfUndefinedDates, - }, - ], - }, - ...getCategories(tagCategories), - ] - - // Remove filters with no items - return availableFilters.filter((filter) => filter.items.length > 0) -} - -export const getFilteredItems = ( - items: ProcessedCollectionCardProps[], - appliedFilters: AppliedFilter[], - searchValue: string, -): ProcessedCollectionCardProps[] => { - return items.filter((item) => { - // Step 1: Filter based on search value - if ( - searchValue !== "" && - !item.title.toLowerCase().includes(searchValue.toLowerCase()) && - !item.description.toLowerCase().includes(searchValue.toLowerCase()) - ) { - return false - } - - // Step 2: Remove items that do not match the applied category filters - const categoryFilter = appliedFilters.find( - (filter) => filter.id === FILTER_ID_CATEGORY, - ) - if ( - categoryFilter && - !categoryFilter.items.some( - (filterItem) => filterItem.id === item.category.toLowerCase(), - ) - ) { - return false - } - - // Step 3: Remove items that do not match the applied year filters - const yearFilter = appliedFilters.find( - (filter) => filter.id === FILTER_ID_YEAR, - ) - if ( - yearFilter && - !yearFilter.items.some((filterItem) => - item.lastUpdated - ? // if date is defined, check if year matches - new Date(item.lastUpdated).getFullYear().toString() === - filterItem.id - : // if undefined date, check if "not specified" filter is applied - filterItem.id === NO_SPECIFIED_YEAR_FILTER_ID, - ) - ) { - return false - } - - const remainingFilters = appliedFilters.filter( - ({ id }) => id !== FILTER_ID_CATEGORY && id !== FILTER_ID_YEAR, - ) - - // Step 4: Compute set intersection between remaining filters and the set of items. - // Take note that we use OR between items within the same filter and AND between filters. - return remainingFilters - .map(({ items: activeFilters, id }) => { - return item.tags?.some(({ category, selected: itemLabels }) => { - return ( - category === id && - activeFilters - .map(({ id }) => id) - .reduce((prev, cur) => { - return prev || itemLabels.includes(cur) - }, false) //includes(itemLabels) - ) - }) - }) - .every((x) => x) - }) -} - -export const getPaginatedItems = ( - items: ProcessedCollectionCardProps[], - itemsPerPage: number, - currPage: number, -) => { - const normalizedCurrPage = Math.max(1, currPage) - - return items.slice( - (normalizedCurrPage - 1) * itemsPerPage, - normalizedCurrPage * itemsPerPage, - ) -} - -export const updateAppliedFilters = ( - appliedFilters: AppliedFilter[], - setAppliedFilters: (appliedFilters: AppliedFilter[]) => void, - filterId: string, - itemId: string, -) => { - const filterIndex = appliedFilters.findIndex( - (filter) => filter.id === filterId, - ) - const isFilterAlreadyApplied = filterIndex > -1 - if (isFilterAlreadyApplied) { - const itemIndex = appliedFilters[filterIndex]?.items.findIndex( - (item) => item.id === itemId, - ) - if (itemIndex !== undefined && itemIndex > -1) { - const newAppliedFilters = [...appliedFilters] - newAppliedFilters[filterIndex]?.items.splice(itemIndex, 1) - - if (newAppliedFilters[filterIndex]?.items.length === 0) { - newAppliedFilters.splice(filterIndex, 1) - } - - setAppliedFilters(newAppliedFilters) - } else { - const newAppliedFilters = [...appliedFilters] - newAppliedFilters[filterIndex]?.items.push({ id: itemId }) - setAppliedFilters(newAppliedFilters) - } - } else { - setAppliedFilters([ - ...appliedFilters, - { id: filterId, items: [{ id: itemId }] }, - ]) - } -} - -export const shouldShowDate = ( - items: ProcessedCollectionCardProps[], -): boolean => { - return items.some((item) => item.lastUpdated) -} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getCategoryFilter.test.ts b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getCategoryFilter.test.ts new file mode 100644 index 0000000000..0a9aa45838 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getCategoryFilter.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest" + +import type { ProcessedCollectionCardProps } from "~/interfaces" +import { getCategoryFilter } from "../getCategoryFilter" + +describe("getCategoryFilter", () => { + it("should return empty filter items when no items provided", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [] + + // Act + const result = getCategoryFilter(items) + + // Assert + expect(result).toEqual({ + id: "category", + label: "Category", + items: [], + }) + }) + + it("should count and format categories correctly", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [ + { + category: "guides", + } as ProcessedCollectionCardProps, + { + category: "guides", + } as ProcessedCollectionCardProps, + { + category: "articles", + } as ProcessedCollectionCardProps, + { + category: "tutorials", + } as ProcessedCollectionCardProps, + { + category: "tutorials", + } as ProcessedCollectionCardProps, + ] + + // Act + const result = getCategoryFilter(items) + + // Assert + expect(result).toEqual({ + id: "category", + label: "Category", + items: [ + { id: "articles", label: "Articles", count: 1 }, + { id: "guides", label: "Guides", count: 2 }, + { id: "tutorials", label: "Tutorials", count: 2 }, + ], + }) + }) +}) diff --git a/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getTagFilters.test.ts b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getTagFilters.test.ts new file mode 100644 index 0000000000..41eb4e0da1 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getTagFilters.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest" + +import type { ProcessedCollectionCardProps } from "~/interfaces" +import { getTagFilters } from "../getTagFilters" + +describe("getTagFilters", () => { + it("returns filters grouped by tag category", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [ + { + title: "Item 1", + tags: [ + { selected: ["Brain", "Heart"], category: "Body parts" }, + { selected: ["Acute"], category: "Condition" }, + ], + category: "category1", + } as ProcessedCollectionCardProps, + { + title: "Item 2", + tags: [ + { selected: ["Brain"], category: "Body parts" }, + { selected: ["Chronic"], category: "Condition" }, + ], + category: "category2", + } as ProcessedCollectionCardProps, + ] + + // Act + const result = getTagFilters(items) + + // Assert + expect(result).toEqual([ + { + id: "Body parts", + label: "Body parts", + items: [ + { id: "Brain", label: "Brain", count: 2 }, + { id: "Heart", label: "Heart", count: 1 }, + ], + }, + { + id: "Condition", + label: "Condition", + items: [ + { id: "Acute", label: "Acute", count: 1 }, + { id: "Chronic", label: "Chronic", count: 1 }, + ], + }, + ]) + }) + + it("returns empty array when no items have tags", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [ + { + title: "Item 1", + description: "Description 1", + category: "category1", + } as ProcessedCollectionCardProps, + ] + + // Act + const result = getTagFilters(items) + + // Assert + expect(result).toEqual([]) + }) + + it("returns empty array for empty input", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [] + + // Act + const result = getTagFilters(items) + + // Assert + expect(result).toEqual([]) + }) +}) diff --git a/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getYearFilter.test.ts b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getYearFilter.test.ts new file mode 100644 index 0000000000..bcfd513099 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/getYearFilter.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest" + +import type { ProcessedCollectionCardProps } from "~/interfaces" +import { NO_SPECIFIED_YEAR_FILTER_ID } from "../constants" +import { getYearFilter } from "../getYearFilter" + +describe("getYearFilter", () => { + it("should return empty filter items when no items provided", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [] + + // Act + const result = getYearFilter(items) + + // Assert + expect(result).toEqual({ + id: "year", + label: "Year", + items: [], + }) + }) + + it("should count and format years correctly", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [ + { + lastUpdated: "2023-01-01", + } as ProcessedCollectionCardProps, + { + lastUpdated: "2023-06-15", + } as ProcessedCollectionCardProps, + { + lastUpdated: "2022-12-31", + } as ProcessedCollectionCardProps, + { + lastUpdated: "2022-01-01", + } as ProcessedCollectionCardProps, + { + lastUpdated: undefined, + } as ProcessedCollectionCardProps, + ] + + // Act + const result = getYearFilter(items) + + // Assert + expect(result).toEqual({ + id: "year", + label: "Year", + items: [ + { id: "2023", label: "2023", count: 2 }, + { id: "2022", label: "2022", count: 2 }, + { id: NO_SPECIFIED_YEAR_FILTER_ID, label: "Not specified", count: 1 }, + ], + }) + }) + + it("should return a single item if all items have the same year", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [ + { lastUpdated: "2023-01-01" } as ProcessedCollectionCardProps, + { lastUpdated: "2023-01-01" } as ProcessedCollectionCardProps, + { lastUpdated: "2023-01-01" } as ProcessedCollectionCardProps, + ] + + // Act + const result = getYearFilter(items) + + // Assert + expect(result).toEqual({ + id: "year", + label: "Year", + items: [{ id: "2023", label: "2023", count: 3 }], + }) + }) + + it("should not return any items if all items have no dates", () => { + // Arrange + const items: ProcessedCollectionCardProps[] = [ + { lastUpdated: undefined } as ProcessedCollectionCardProps, + { lastUpdated: undefined } as ProcessedCollectionCardProps, + { lastUpdated: undefined } as ProcessedCollectionCardProps, + ] + + // Act + const result = getYearFilter(items) + + // Assert + expect(result).toEqual({ + id: "year", + label: "Year", + items: [], + }) + }) +}) diff --git a/packages/components/src/templates/next/layouts/Collection/utils/__tests__/shouldShowDate.test.ts b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/shouldShowDate.test.ts new file mode 100644 index 0000000000..2914e8dc9d --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/__tests__/shouldShowDate.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest" + +import type { ProcessedCollectionCardProps } from "~/interfaces" +import { shouldShowDate } from "../shouldShowDate" + +describe("shouldShowDate", () => { + it("returns true if any item has lastUpdated", () => { + const items = [ + { + title: "Item 1", + description: "Description 1", + lastUpdated: "2023-01-01", + category: "category1", + } as ProcessedCollectionCardProps, + { + title: "Item 2", + description: "Description 2", + lastUpdated: undefined, + category: "category2", + } as ProcessedCollectionCardProps, + ] + + expect(shouldShowDate(items)).toBe(true) + }) + + it("returns false if no items have lastUpdated", () => { + const items = [ + { + title: "Item 1", + description: "Description 1", + lastUpdated: undefined, + category: "category1", + } as ProcessedCollectionCardProps, + { + title: "Item 2", + description: "Description 2", + lastUpdated: undefined, + category: "category2", + } as ProcessedCollectionCardProps, + ] + + expect(shouldShowDate(items)).toBe(false) + }) + + it("returns false for empty array", () => { + expect(shouldShowDate([])).toBe(false) + }) +}) diff --git a/packages/components/src/templates/next/layouts/Collection/utils/constants.ts b/packages/components/src/templates/next/layouts/Collection/utils/constants.ts new file mode 100644 index 0000000000..0718a16209 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/constants.ts @@ -0,0 +1,3 @@ +export const FILTER_ID_CATEGORY = "category" +export const FILTER_ID_YEAR = "year" +export const NO_SPECIFIED_YEAR_FILTER_ID = "not_specified" diff --git a/packages/components/src/templates/next/layouts/Collection/utils/getAvailableFilters.ts b/packages/components/src/templates/next/layouts/Collection/utils/getAvailableFilters.ts new file mode 100644 index 0000000000..2e8530fe01 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/getAvailableFilters.ts @@ -0,0 +1,15 @@ +import type { Filter } from "../../../types/Filter" +import type { ProcessedCollectionCardProps } from "~/interfaces" +import { getCategoryFilter } from "./getCategoryFilter" +import { getTagFilters } from "./getTagFilters" +import { getYearFilter } from "./getYearFilter" + +export const getAvailableFilters = ( + items: ProcessedCollectionCardProps[], +): Filter[] => { + return [ + getCategoryFilter(items), + getYearFilter(items), + ...getTagFilters(items), + ].filter((filter) => filter.items.length >= 1) +} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/getCategoryFilter.ts b/packages/components/src/templates/next/layouts/Collection/utils/getCategoryFilter.ts new file mode 100644 index 0000000000..f231ff09fa --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/getCategoryFilter.ts @@ -0,0 +1,31 @@ +import type { ProcessedCollectionCardProps } from "~/interfaces" +import type { Filter } from "~/templates/next/types/Filter" +import { FILTER_ID_CATEGORY } from "./constants" + +export const getCategoryFilter = ( + items: ProcessedCollectionCardProps[], +): Filter => { + const categories: Record = {} + + items.forEach(({ category }) => { + if (category in categories && categories[category]) { + categories[category] += 1 + } else { + categories[category] = 1 + } + }) + + const categoryFilterItems = Object.entries(categories) + .map(([label, count]) => ({ + id: label.toLowerCase(), + label: label.charAt(0).toUpperCase() + label.slice(1), + count, + })) + .sort((a, b) => a.label.localeCompare(b.label)) + + return { + id: FILTER_ID_CATEGORY, + label: "Category", + items: categoryFilterItems, + } +} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/getFilteredItems.ts b/packages/components/src/templates/next/layouts/Collection/utils/getFilteredItems.ts new file mode 100644 index 0000000000..65cb3a901e --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/getFilteredItems.ts @@ -0,0 +1,76 @@ +import type { AppliedFilter } from "../../../types/Filter" +import type { ProcessedCollectionCardProps } from "~/interfaces" +import { + FILTER_ID_CATEGORY, + FILTER_ID_YEAR, + NO_SPECIFIED_YEAR_FILTER_ID, +} from "./constants" + +export const getFilteredItems = ( + items: ProcessedCollectionCardProps[], + appliedFilters: AppliedFilter[], + searchValue: string, +): ProcessedCollectionCardProps[] => { + return items.filter((item) => { + // Step 1: Filter based on search value + if ( + searchValue !== "" && + !item.title.toLowerCase().includes(searchValue.toLowerCase()) && + !item.description.toLowerCase().includes(searchValue.toLowerCase()) + ) { + return false + } + + // Step 2: Remove items that do not match the applied category filters + const categoryFilter = appliedFilters.find( + (filter) => filter.id === FILTER_ID_CATEGORY, + ) + if ( + categoryFilter && + !categoryFilter.items.some( + (filterItem) => filterItem.id === item.category.toLowerCase(), + ) + ) { + return false + } + + // Step 3: Remove items that do not match the applied year filters + const yearFilter = appliedFilters.find( + (filter) => filter.id === FILTER_ID_YEAR, + ) + if ( + yearFilter && + !yearFilter.items.some((filterItem) => + item.lastUpdated + ? // if date is defined, check if year matches + new Date(item.lastUpdated).getFullYear().toString() === + filterItem.id + : // if undefined date, check if "not specified" filter is applied + filterItem.id === NO_SPECIFIED_YEAR_FILTER_ID, + ) + ) { + return false + } + + const remainingFilters = appliedFilters.filter( + ({ id }) => id !== FILTER_ID_CATEGORY && id !== FILTER_ID_YEAR, + ) + + // Step 4: Compute set intersection between remaining filters and the set of items. + // Take note that we use OR between items within the same filter and AND between filters. + return remainingFilters + .map(({ items: activeFilters, id }) => { + return item.tags?.some(({ category, selected: itemLabels }) => { + return ( + category === id && + activeFilters + .map(({ id }) => id) + .reduce((prev, cur) => { + return prev || itemLabels.includes(cur) + }, false) //includes(itemLabels) + ) + }) + }) + .every((x) => x) + }) +} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/getPaginatedItems.ts b/packages/components/src/templates/next/layouts/Collection/utils/getPaginatedItems.ts new file mode 100644 index 0000000000..347b081b85 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/getPaginatedItems.ts @@ -0,0 +1,14 @@ +import type { ProcessedCollectionCardProps } from "~/interfaces" + +export const getPaginatedItems = ( + items: ProcessedCollectionCardProps[], + itemsPerPage: number, + currPage: number, +) => { + const normalizedCurrPage = Math.max(1, currPage) + + return items.slice( + (normalizedCurrPage - 1) * itemsPerPage, + normalizedCurrPage * itemsPerPage, + ) +} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/getTagFilters.ts b/packages/components/src/templates/next/layouts/Collection/utils/getTagFilters.ts new file mode 100644 index 0000000000..a9e6c8138e --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/getTagFilters.ts @@ -0,0 +1,53 @@ +import type { Filter, FilterItem } from "../../../types/Filter" +import type { ProcessedCollectionCardProps } from "~/interfaces" + +export const getTagFilters = ( + items: ProcessedCollectionCardProps[], +): Filter[] => { + // NOTE: Each tag is a mapping of a category to its + // associated set of values as well as the selected value. + // Hence, we store a map here of the category (eg: Body parts) + // to the number of occurences of each value (eg: { Brain: 3, Leg: 2}) + const tagCategories: Record> = {} + + items.forEach(({ tags }) => { + if (tags) { + tags.forEach(({ selected: selectedLabels, category }) => { + selectedLabels.forEach((label) => { + if (!tagCategories[category]) { + tagCategories[category] = {} + } + if (!tagCategories[category][label]) { + tagCategories[category][label] = 0 + } + + tagCategories[category][label] += 1 + }) + }) + } + }) + + return Object.entries(tagCategories) + .reduce((acc: Filter[], curValue) => { + const [category, values] = curValue + const items: FilterItem[] = Object.entries(values) + .map(([label, count]) => ({ + label, + count, + id: label, + })) + .sort((a, b) => a.label.localeCompare(b.label)) + + const filters: Filter[] = [ + ...acc, + { + items, + id: category, + label: category, + }, + ] + + return filters + }, []) + .sort((a, b) => a.label.localeCompare(b.label)) +} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/getYearFilter.ts b/packages/components/src/templates/next/layouts/Collection/utils/getYearFilter.ts new file mode 100644 index 0000000000..ea960352a1 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/getYearFilter.ts @@ -0,0 +1,51 @@ +import type { Filter } from "../../../types/Filter" +import type { ProcessedCollectionCardProps } from "~/interfaces" +import { getParsedDate } from "~/utils" +import { FILTER_ID_YEAR, NO_SPECIFIED_YEAR_FILTER_ID } from "./constants" + +export const getYearFilter = ( + items: ProcessedCollectionCardProps[], +): Filter => { + const years: Record = {} + let numberOfUndefinedDates = 0 + + items.forEach(({ lastUpdated }) => { + if (lastUpdated) { + const year = getParsedDate(lastUpdated).getFullYear().toString() + if (year in years && years[year]) { + years[year] += 1 + } else { + years[year] = 1 + } + } else { + numberOfUndefinedDates += 1 + } + }) + + const yearFilterItems = Object.entries(years) + .map(([label, count]) => ({ + id: label.toLowerCase(), + label, + count, + })) + .sort((a, b) => parseInt(b.label) - parseInt(a.label)) + + return { + id: FILTER_ID_YEAR, + label: "Year", + // do not show "not specified" option if all items have undefined dates + items: + yearFilterItems.length === 0 + ? [] + : numberOfUndefinedDates === 0 + ? yearFilterItems + : [ + ...yearFilterItems, + { + id: NO_SPECIFIED_YEAR_FILTER_ID, + label: "Not specified", + count: numberOfUndefinedDates, + }, + ], + } +} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/index.ts b/packages/components/src/templates/next/layouts/Collection/utils/index.ts new file mode 100644 index 0000000000..b8292bddf8 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./getAvailableFilters" +export * from "./getFilteredItems" +export * from "./getPaginatedItems" +export * from "./updateAppliedFilters" +export * from "./shouldShowDate" diff --git a/packages/components/src/templates/next/layouts/Collection/utils/shouldShowDate.ts b/packages/components/src/templates/next/layouts/Collection/utils/shouldShowDate.ts new file mode 100644 index 0000000000..7ccc053963 --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/shouldShowDate.ts @@ -0,0 +1,7 @@ +import type { ProcessedCollectionCardProps } from "~/interfaces" + +export const shouldShowDate = ( + items: ProcessedCollectionCardProps[], +): boolean => { + return items.some((item) => item.lastUpdated) +} diff --git a/packages/components/src/templates/next/layouts/Collection/utils/updateAppliedFilters.ts b/packages/components/src/templates/next/layouts/Collection/utils/updateAppliedFilters.ts new file mode 100644 index 0000000000..c22037a8bf --- /dev/null +++ b/packages/components/src/templates/next/layouts/Collection/utils/updateAppliedFilters.ts @@ -0,0 +1,37 @@ +import type { AppliedFilter } from "../../../types/Filter" + +export const updateAppliedFilters = ( + appliedFilters: AppliedFilter[], + setAppliedFilters: (appliedFilters: AppliedFilter[]) => void, + filterId: string, + itemId: string, +) => { + const filterIndex = appliedFilters.findIndex( + (filter) => filter.id === filterId, + ) + const isFilterAlreadyApplied = filterIndex > -1 + if (isFilterAlreadyApplied) { + const itemIndex = appliedFilters[filterIndex]?.items.findIndex( + (item) => item.id === itemId, + ) + if (itemIndex !== undefined && itemIndex > -1) { + const newAppliedFilters = [...appliedFilters] + newAppliedFilters[filterIndex]?.items.splice(itemIndex, 1) + + if (newAppliedFilters[filterIndex]?.items.length === 0) { + newAppliedFilters.splice(filterIndex, 1) + } + + setAppliedFilters(newAppliedFilters) + } else { + const newAppliedFilters = [...appliedFilters] + newAppliedFilters[filterIndex]?.items.push({ id: itemId }) + setAppliedFilters(newAppliedFilters) + } + } else { + setAppliedFilters([ + ...appliedFilters, + { id: filterId, items: [{ id: itemId }] }, + ]) + } +}