diff --git a/src/custom/CatalogFilterSection/CatalogFilterSidebar.tsx b/src/custom/CatalogFilterSection/CatalogFilterSidebar.tsx new file mode 100644 index 000000000..8cc176221 --- /dev/null +++ b/src/custom/CatalogFilterSection/CatalogFilterSidebar.tsx @@ -0,0 +1,125 @@ +import FilterAltIcon from '@mui/icons-material/FilterAlt'; +import { useTheme } from '@mui/material/styles'; +import { useCallback, useState } from 'react'; +import { Box, Drawer, Typography } from '../../base'; +import { CloseIcon } from '../../icons'; +import { CloseBtn } from '../Modal'; +import CatalogFilterSidebarState from './CatalogFilterSidebarState'; +import { + FilterButton, + FilterDrawerDiv, + FilterText, + FiltersCardDiv, + FiltersDrawerHeader +} from './style'; + +export interface FilterOption { + value: string; + label: string; + totalCount?: number; + description?: string; + Icon?: React.ComponentType<{ + width: string; + height: string; + }>; +} + +export interface FilterList { + filterKey: string; + sectionDisplayName?: string; + options: FilterOption[]; + defaultOpen?: boolean; + isMultiSelect?: boolean; +} + +export interface CatalogFilterSidebarProps { + setData: (callback: (prevFilters: FilterValues) => FilterValues) => void; + lists: FilterList[]; + value?: FilterValues; +} + +export type FilterValues = Record; + +export interface StyleProps { + backgroundColor?: string; + sectionTitleBackgroundColor?: string; +} + +/** + * @function CatalogFilterSidebar + * @description A functional component that renders the filter sidebar. + * @param {Array} value - The data to be filtered. + * @param {Function} setData - A function to set the filtered data. + * @param {Array} lists - An array of filter sections and its options lists. + */ +const CatalogFilterSidebar: React.FC = ({ + lists, + setData, + value = {} +}) => { + const theme = useTheme(); // Get the current theme + const [openDrawer, setOpenDrawer] = useState(false); + + const handleDrawerOpen = useCallback(() => { + setOpenDrawer(true); + }, []); + + const handleDrawerClose = useCallback(() => { + setOpenDrawer(false); + }, []); + + const styleProps: StyleProps = { + backgroundColor: theme.palette.background.default, + sectionTitleBackgroundColor: theme.palette.background.surfaces + }; + + return ( + <> + + + + + + + Filters + + + + + + + Filters + + + + + + + + + + {/* Use theme-aware color */} + + + + + ); +}; + +export default CatalogFilterSidebar; diff --git a/src/custom/CatalogFilterSection/CatalogFilterSidebarState.tsx b/src/custom/CatalogFilterSection/CatalogFilterSidebarState.tsx new file mode 100644 index 000000000..76b0d72d3 --- /dev/null +++ b/src/custom/CatalogFilterSection/CatalogFilterSidebarState.tsx @@ -0,0 +1,98 @@ +import { useCallback, useState } from 'react'; +import { + CatalogFilterSidebarProps, + FilterList, + FilterValues, + StyleProps +} from './CatalogFilterSidebar'; +import FilterSection from './FilterSection'; + +/** + * @component CatalogFilterSidebarState + * @description A functional component that manages the filter state. + * @param {Array} lists - An array of filter sections and its options lists. + * @param {Function} onApplyFilters - A function to apply the filters. + * @param {Object} value - The selected filters. + * @param {Object} styleProps - The style properties for the component. + */ +const CatalogFilterSidebarState: React.FC<{ + lists: FilterList[]; + onApplyFilters: CatalogFilterSidebarProps['setData']; + value: FilterValues; + styleProps: StyleProps; +}> = ({ lists, onApplyFilters, value, styleProps }) => { + // Generate initial state with all sections open by default + const [openSections, setOpenSections] = useState>(() => { + const initialOpenSections: Record = {}; + lists.forEach((list) => { + initialOpenSections[list.filterKey] = !!list.defaultOpen; + }); + return initialOpenSections; + }); + + /** + * @function handleSectionToggle + * @description Handles the section toggle event. + * @param {string} filterKey - The name of the filter section. + */ + const handleSectionToggle = useCallback((filterKey: string) => { + setOpenSections((prevOpenSections) => ({ + ...prevOpenSections, + [filterKey]: !prevOpenSections[filterKey] + })); + }, []); + + /** + * @function handleCheckboxChange + * @description Handles the checkbox change event. + * @param {string} filterKey - The name of the filter section. + * @param {string} value - The value of the checkbox. + * @param {boolean} checked - The checked state of the checkbox. + */ + const handleCheckboxChange = useCallback( + (filterKey: string, value: string, checked: boolean) => { + onApplyFilters((prevFilters) => { + const updatedFilters = { ...prevFilters }; + const filterList = lists.find((list) => list.filterKey === filterKey); + + // default is multi select + if (filterList?.isMultiSelect !== false) { + let currentValues = updatedFilters[filterKey] as string[] | undefined; + + if (!Array.isArray(currentValues)) { + currentValues = currentValues ? [currentValues as string] : []; // convert to array; + } + + updatedFilters[filterKey] = checked + ? [...currentValues, value] + : currentValues.filter((item) => item !== value); + } else { + updatedFilters[filterKey] = checked ? value : ''; + } + + return updatedFilters; + }); + }, + [lists, onApplyFilters] + ); + + return ( + <> + {lists.map((list) => ( + + ))} + + ); +}; + +export default CatalogFilterSidebarState; diff --git a/src/custom/CatalogFilterSection/FilterSection.tsx b/src/custom/CatalogFilterSection/FilterSection.tsx new file mode 100644 index 000000000..af937876f --- /dev/null +++ b/src/custom/CatalogFilterSection/FilterSection.tsx @@ -0,0 +1,142 @@ +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useCallback, useState } from 'react'; +import { + Box, + Checkbox, + Collapse, + InputAdornment, + List, + OutlinedInput, + Stack, + Typography +} from '../../base'; +import { SearchIcon } from '../../icons'; +import { InfoTooltip } from '../CustomTooltip'; +import { FilterOption, FilterValues, StyleProps } from './CatalogFilterSidebar'; +import { FilterTitleButton, InputAdornmentEnd } from './style'; + +interface FilterSectionProps { + filterKey: string; + sectionDisplayName?: string; + options: FilterOption[]; + filters: FilterValues; + openSections: Record; + onCheckboxChange: (filterKey: string, value: string, checked: boolean) => void; + onSectionToggle: (filterKey: string) => void; + styleProps: StyleProps; +} + +/** + * @component FilterSection + * @description A functional component that renders a filter section. + * @param {string} filterKey - The key of the filter section. + * @param {string} sectionDisplayName - The title of the filter section. + * @param {Array} options - The available options for the filter section. + * @param {Object} filters - The selected filters. + * @param {Object} openSections - The open/closed state of the filter sections. + * @param {Function} onCheckboxChange - A function to handle checkbox change event. + * @param {Function} onSectionToggle - A function to handle section toggle event. + * @param {Object} styleProps - The style properties for the component. + */ +const FilterSection: React.FC = ({ + filterKey, + sectionDisplayName, + options, + filters, + openSections, + onCheckboxChange, + onSectionToggle, + styleProps +}) => { + const [searchQuery, setSearchQuery] = useState(''); + + const handleTextFieldChange = useCallback((e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }, []); + + const showSearch = options.length > 10; + const searchedOptions = searchQuery + ? options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())) + : options; + + return ( + <> + onSectionToggle(filterKey)} + style={{ backgroundColor: styleProps.sectionTitleBackgroundColor }} + > + + {(sectionDisplayName || filterKey).toUpperCase()} + + {openSections[filterKey] ? : } + + + + {showSearch && ( + + + + + } + endAdornment={ + + Total: {searchedOptions.length} + + } + /> + + )} + {searchedOptions.map((option, index) => ( + + + onCheckboxChange(filterKey, option.value, e.target.checked)} + value={option.value} + /> + + {option.Icon && } + + {option.label} + + + {option.totalCount !== undefined && `(${option.totalCount || 0})`} + {option.description && ( + + )} + + + ))} + + + + ); +}; + +export default FilterSection; diff --git a/src/custom/CatalogFilterSection/index.tsx b/src/custom/CatalogFilterSection/index.tsx new file mode 100644 index 000000000..bab5baf47 --- /dev/null +++ b/src/custom/CatalogFilterSection/index.tsx @@ -0,0 +1,3 @@ +import CatalogFilterSidebar from './CatalogFilterSidebar'; + +export { CatalogFilterSidebar }; diff --git a/src/custom/CatalogFilterSection/style.tsx b/src/custom/CatalogFilterSection/style.tsx new file mode 100644 index 000000000..d722a0c3a --- /dev/null +++ b/src/custom/CatalogFilterSection/style.tsx @@ -0,0 +1,84 @@ +import { styled } from '@mui/material/styles'; +import { Box, Button, InputAdornment, ListItemButton } from '../../base'; +import { StyleProps } from './CatalogFilterSidebar'; + +export const FiltersCardDiv = styled(Box)<{ styleProps: StyleProps }>(({ styleProps }) => ({ + padding: '1rem', + borderRadius: '1rem', + width: '100%', + gap: '0.5rem', + boxShadow: '0px 2px 10px rgba(0, 0, 0, 0.2)', + display: 'flex', + flexDirection: 'column', + height: 'fit-content', + backgroundColor: styleProps.backgroundColor, + ['@media (max-width:900px)']: { + display: 'none' + } +})); + +export const FilterDrawerDiv = styled('div')(() => ({ + display: 'none', + ['@media (max-width:899px)']: { + display: 'block' + } +})); + +export const LabelDiv = styled('div')(() => ({ + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center' +})); + +export const FilterButton = styled(Button)(({ theme }) => ({ + backgroundColor: theme.palette.primary.brand?.default, + '&:hover': { + backgroundColor: theme.palette.background.default + }, + height: '3.5rem', + ['@media (max-width:450px)']: { + minWidth: '0px' + } +})); + +export const FiltersDrawerHeader = styled(Box)(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '0.5rem 1rem', + backgroundColor: theme.palette.border.strong, + height: '10vh', + boxShadow: '0px 4px 4px rgba(0, 179, 159, 0.4)', + marginBottom: '0.625rem' +})); + +export const CheckBoxButton = styled(ListItemButton)(({ theme }) => ({ + padding: '0.25rem 2rem', + borderBottom: '1px solid', + borderBottomColor: theme.palette.text.disabled +})); + +export const FilterTitleButton = styled(ListItemButton)(({ theme }) => ({ + backgroundColor: theme.palette.background.surfaces, + borderRadius: '0.5rem', + marginTop: 2, + display: 'flex', + justifyContent: 'space-between' +})); + +export const InputAdornmentEnd = styled(InputAdornment)(({ theme }) => ({ + borderLeft: `1px solid ${theme.palette.text.disabled}`, + height: '30px', + paddingLeft: '10px', + '@media (max-width: 590px)': { + paddingLeft: '0px' + } +})); + +export const FilterText = styled('span')(() => ({ + marginLeft: '0.5rem', + display: 'block', + '@media (max-width: 853px)': { + display: 'none' + } +})); diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 45c772aab..8c57e1a92 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -43,6 +43,7 @@ import { TransferList } from './TransferModal/TransferList'; import { TransferListProps } from './TransferModal/TransferList/TransferList'; import UniversalFilter, { UniversalFilterProps } from './UniversalFilter'; export { CatalogCard } from './CatalogCard'; +export { CatalogFilterSidebar } from './CatalogFilterSection'; export { StyledChartDialog } from './ChartDialog'; export { LearningContent } from './LearningContent'; export { Note } from './Note';