import { TypeGuards } from '@codeleap/types' import { deepEqual } from '@codeleap/utils' import React, { useCallback, useMemo } from 'react' export type TSectionFilterItem = { value?: string | number label?: string } type Section = { data: T[] title: string selectionLimit?: number disableItemsOnLimitReached?: boolean } type SelectedItemsPerSection = { [X: number]: T[] } export type UseSectionFilters = { sections: Section[] areItemsEqual?: (a: T, b: T) => boolean selectionLimit?: number sectionSelectionLimit?: number disableItemsOnLimitReached?: boolean initialSelectedItems?: SelectedItemsPerSection onToggleItem?: (item: T, section: Section) => boolean } /** * All selection state is owned internally — this hook is uncontrolled. * Pass `initialSelectedItems` to seed state; after mount, changes are only possible * through `toggleItem` / `clearSelectedItemsWithSection`. * `onToggleItem` can intercept a toggle and return `true` to prevent the internal * state update (useful for controlled scenarios without rewriting the whole hook). * When `selectionLimit === 1` the entire per-section array is replaced on toggle * rather than appended, so de-selecting by re-tapping the same item is not supported * at limit=1 — tapping a new item simply replaces the previous selection. */ export function useSectionFilters(props: UseSectionFilters) { const { sections, areItemsEqual = deepEqual, selectionLimit = 1, sectionSelectionLimit = null, disableItemsOnLimitReached = selectionLimit > 1 && !sectionSelectionLimit, initialSelectedItems = [], onToggleItem, } = props /** When `initialSelectedItems` is a plain array (no section keys), it is assigned to * section index 0 so single-section callers don't have to wrap in an object. */ const [selectedItems, setSelectedItems] = React.useState>(() => { if (TypeGuards.isArray(initialSelectedItems)) { return { 0: initialSelectedItems, } } return initialSelectedItems ?? {} }) const changed = useCallback(() => { return Object.entries(selectedItems).some(([sectionIndex, items]) => { const initialItems = initialSelectedItems[sectionIndex] ?? [] if (!initialItems) { return items.length > 0 } if (items.length !== initialItems.length) { return true } return items.some((item) => !initialItems.some((i) => areItemsEqual(i, item))) }) }, [selectedItems, initialSelectedItems]) const getAllItems = () => { return sections?.flatMap((section) => section.data) ?? [] } const findItemSection = (item: T) => { if (!sections) { return { sectionIndex: 0, section: null, } } const sectionIndex = sections?.findIndex((section) => { return section.data.some((i) => areItemsEqual(item, i)) }) if (sectionIndex === -1) { return { sectionIndex: null, section: null, } } const section = sections[sectionIndex] return { sectionIndex, section, } } const isSelected = (item: T) => { if (sections) { const { sectionIndex } = findItemSection(item) return selectedItems[sectionIndex]?.some((i) => areItemsEqual(i, item)) } return selectedItems[0]?.some((i) => areItemsEqual(i, item)) } const toggleItem = (item: T) => { let sectionIndex = -1 let limit = selectionLimit if (sections) { const { sectionIndex: si, section } = findItemSection(item) if (si === null) { return } sectionIndex = si limit = section.selectionLimit ?? selectionLimit } else { sectionIndex = 0 } const handled = onToggleItem?.(item, sections[sectionIndex]) ?? false if (handled) { return } if (selectionLimit === 1) { setSelectedItems({ [sectionIndex]: [item], }) return } const currentItems = selectedItems[sectionIndex] ?? [] const isItemSelected = currentItems.some((i) => areItemsEqual(i, item)) const newItems = [...currentItems] if (isItemSelected) { const index = newItems.findIndex((i) => areItemsEqual(i, item)) newItems.splice(index, 1) } else { if (newItems.length >= limit) { newItems.shift() } newItems.push(item) } setSelectedItems({ ...selectedItems, [sectionIndex]: newItems, }) } function sectionLimitReached(sectionIndex: number) { const section = sections[sectionIndex] if (!section) { return false } const limit = section.selectionLimit ?? sectionSelectionLimit ?? selectionLimit if (!limit) { return false } const nItems = selectedItems[sectionIndex]?.length return nItems >= limit } function limitReached() { const nItems = Object.values(selectedItems).flatMap((i) => i).length return nItems >= selectionLimit } function clearSelectedItemsWithSection(sectionIndex: number) { setSelectedItems({ ...selectedItems, [sectionIndex]: [], }) } return { isSelected, toggleItem, findItemSection, selectedItems, sectionLimitReached, limitReached, disableItemsOnLimitReached, clearSelectedItemsWithSection, changed, areItemsEqual, getAllItems, } }