diff --git a/packages/examples-site/src/App.tsx b/packages/examples-site/src/App.tsx index 15fb501..3afbfb9 100644 --- a/packages/examples-site/src/App.tsx +++ b/packages/examples-site/src/App.tsx @@ -16,7 +16,8 @@ import InstallScreenApp from "./pages/InstallScreenPage/AppExample"; import InstallScreenChannel from "./pages/InstallScreenPage/ChannelExample"; import PageFiltersSearch from "./pages/FiltersSearchPage/FiltersSearchPage"; import PageFiltersDropdowns from "./pages/FiltersDropdownsPage/FiltersDropdownsPage"; -import PageFiltersAdvanced from "./pages/FiltersAdvancedPAge/FiltersAdvancedPage"; +import PageFiltersAdvanced from "./pages/FiltersAdvancedPage/FiltersAdvancedPage"; +import PageFiltersAdvancedAdditive from "./pages/FiltersAdvancedAdditivePage/FiltersAdvancedAdditivePage"; export const alertsManager = createAlertsManager(); @@ -39,6 +40,10 @@ const RouteFC = () => { { path: "/filters-search", element: }, { path: "/filters-dropdowns", element: }, { path: "/filters-advanced", element: }, + { + path: "/filters-advanced-additive", + element: , + }, ]); return routes; }; diff --git a/packages/examples-site/src/pages/FiltersAdvancedAdditivePage/FiltersAdvancedAdditivePage.styled.tsx b/packages/examples-site/src/pages/FiltersAdvancedAdditivePage/FiltersAdvancedAdditivePage.styled.tsx new file mode 100644 index 0000000..efdc569 --- /dev/null +++ b/packages/examples-site/src/pages/FiltersAdvancedAdditivePage/FiltersAdvancedAdditivePage.styled.tsx @@ -0,0 +1,64 @@ +import { theme as defaultTheme } from "@bigcommerce/big-design-theme"; +import styled from "styled-components"; +import { BoxProps } from "@bigcommerce/big-design"; + +import { GridItem, Link } from "@bigcommerce/big-design"; + +export const StyledGridItem = styled(GridItem)` + align-content: center; +`; + +export const StyledFiltersLink = styled(Link)` + display: inline-flex; + align-items: center; + flex-gap: 0.25rem; +`; + +export const StyledPanelContents = styled.div` + display: block; + box-sizing: border-box; + margin-inline: -${({ theme }) => theme.spacing.medium}; + max-width: calc( + 100% + ${({ theme }) => theme.spacing.medium}px + + ${({ theme }) => theme.spacing.medium}px + ); + overflow-x: auto; + @media (min-width: ${({ theme }) => theme.breakpointValues.tablet}) { + margin-inline: -${({ theme }) => theme.spacing.xLarge}; + max-width: calc( + 100% + ${({ theme }) => theme.spacing.xLarge}px + + ${({ theme }) => theme.spacing.xLarge}px + ); + } +`; + +// Provides default theme props to ensure consistent styling if not provided externally +StyledPanelContents.defaultProps = { theme: defaultTheme }; + +export const StyledProductImage = styled.div` + display: block; + box-sizing: border-box; + width: 47px; + height: 47px; + border: ${({ theme }) => theme.border.box}; + border-radius: ${({ theme }) => theme.borderRadius.normal}; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +StyledProductImage.defaultProps = { theme: defaultTheme }; + + +export const StyledBulkActions = styled.div` +display: block; +@media (min-width: ${({ theme }) => theme.breakpointValues.tablet}) { + min-width: 300px; +} +`; + +StyledBulkActions.defaultProps = { theme: defaultTheme }; \ No newline at end of file diff --git a/packages/examples-site/src/pages/FiltersAdvancedAdditivePage/FiltersAdvancedAdditivePage.tsx b/packages/examples-site/src/pages/FiltersAdvancedAdditivePage/FiltersAdvancedAdditivePage.tsx new file mode 100644 index 0000000..029f6f2 --- /dev/null +++ b/packages/examples-site/src/pages/FiltersAdvancedAdditivePage/FiltersAdvancedAdditivePage.tsx @@ -0,0 +1,593 @@ +import React, { FunctionComponent, useState, useEffect } from "react"; +import { + Flex, + FlexItem, + Box, + Button, + Panel, + Chip, + Table, + TableItem, + Text, + Dropdown, + ProgressCircle, + Input, + Form, + FormGroup, + Grid, + Modal, + Fieldset, + MultiSelect, + Select, +} from "@bigcommerce/big-design"; +import { InfoIllustration } from "bigcommerce-design-patterns"; +import { + ArrowDropDownIcon, + CloseIcon, + FilterListIcon, + MoreHorizIcon, + SearchIcon, +} from "@bigcommerce/big-design-icons"; +import { Header, Page } from "@bigcommerce/big-design-patterns"; +import { useNavigate } from "react-router"; +import { useLocation } from "react-router-dom"; +import { theme } from "@bigcommerce/big-design-theme"; +import { + StyledGridItem, + StyledFiltersLink, + StyledPanelContents, + StyledProductImage, +} from "./FiltersAdvancedAdditivePage.styled"; + +import { DummyItem } from "../../data/dummyProducts"; +import { getCategories, getProducts } from "../../data/services"; +import { Category } from "../../data/dummyCategories"; +import { findCategoryById } from "../../helpers/categories"; +import { formatPrice } from "../../helpers/price"; + +/** + * Mock data for the items to be displayed in the table. + */ +interface Item extends DummyItem, TableItem {} + +/** + * Column definitions for the table. + */ + +/** + * Function to sort the items based on a column and direction. + * @param {Item[]} items - The items to sort. + * @param {string} columnHash - The column to sort by. + * @param {string} direction - The direction to sort (ASC or DESC). + * @returns {Item[]} - The sorted items. + */ +const sort = (items: Item[], columnHash: string, direction: string) => { + return items + .concat() + .sort((a, b) => + direction === "ASC" + ? a[columnHash] >= b[columnHash] + ? 1 + : -1 + : a[columnHash] <= b[columnHash] + ? 1 + : -1 + ); +}; + +/** + * PageList component - Displays a page with a list of items in a table. + */ +const PageFiltersAdvancedAdditive: FunctionComponent = () => { + // NAVIGATION + const location = useLocation(); + + const navigate = useNavigate(); + + // TABLE HEADERS + const columns = [ + { + header: "Name", + hash: "name", + render: ({ name, image }: { name: string; image: string }) => { + const imgSrc = `./assets/images/product-images/${image}`; + return ( + + + {name} + + {name} + + ); + }, + isSortable: true, + }, + { + header: "Sku", + hash: "sku", + render: ({ sku }: { sku: string }) => sku, + isSortable: true, + }, + { + header: "Categories", + hash: "categories", + render: ({ categories }: { categories: number[] }) => { + // get category labels from a deep object and join them + return categories + .map((categoryId) => { + const category = findCategoryById(productCats, categoryId); + return category ? category.label : ""; + }) + .join(", "); + }, + }, + { + header: "Stock", + hash: "stock", + render: ({ stock }: { stock: number }) => stock, + isSortable: true, + }, + { + header: "Price", + hash: "price", + render: ({ price }: { price: number }) => formatPrice(price), + isSortable: true, + }, + { + header: null, + hash: "actions", + render: ({ productId, url }: { productId: number; url: string }) => { + return ( + { + return null; + }, + hash: "edsome-action", + }, + { + content: "Some other action", + onItemClick: () => { + return null; + }, + hash: "some-other-action", + }, + ]} + maxHeight={250} + placement="bottom-end" + toggle={ + + } + /> + ); + }, + isSortable: true, + }, + ]; + + // DATA HANDLING + const [currentItems, setCurrentItems] = useState([]); + const [itemsLoaded, setItemsLoaded] = useState(false); + + const setTableItems = ( + themItems: any, + thePage = currentPage, + itemCount = itemsPerPage + ) => { + const maxItems = thePage * itemCount; + const lastItem = Math.min(maxItems, themItems.length); + const firstItem = Math.max(0, maxItems - itemsPerPage); + + setCurrentItems(themItems.slice(firstItem, lastItem)); + }; + + // PAGINATION + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPageOptions] = useState([10, 20, 30]); + const [itemsPerPage, setItemsPerPage] = useState(10); + + const onItemsPerPageChange = (newRange: number) => { + setCurrentPage(1); + setItemsPerPage(newRange); + }; + + const onPageChange = (newPage: number) => { + setCurrentPage(newPage); + setTableItems(items, newPage); + }; + + // SORTING + const [columnHash, setColumnHash] = useState(""); + const [direction, setDirection] = useState<"ASC" | "DESC">("ASC"); + const onSort = (newColumnHash: string, newDirection: "ASC" | "DESC") => { + setColumnHash(newColumnHash); + setDirection(newDirection); + const orderedItems = sort(items, newColumnHash, newDirection); + setTableItems(orderedItems); + }; + + // SEARCH + const [searchValue, setSearchValue] = useState(""); + const onSearchChange = (event: React.ChangeEvent) => { + setSearchValue(event.target.value); + // let's reset the items to the original data if the search value is empty + if (!event.target.value) { + setItems(allItems); + setTableItems(allItems); + } + }; + // search submission handler + const onSearchSubmit = (e) => { + e.preventDefault(); + if (searchValue) { + // let's find the items + const foundItems = items.filter((item) => + item.name.toLowerCase().includes(searchValue.toLowerCase()) + ); + // set the items + setItems(foundItems); + setTableItems(foundItems); + setFiltersApplied(true); + } + }; + + // EFFECTS + + // fetch categories and product all at once + + const [productCats, setProductCats] = useState([]); + const [items, setItems] = useState([]); + const [allItems, setAllItems] = useState([]); + useEffect(() => { + Promise.all([getCategories(), getProducts()]).then( + ([categories, products]) => { + setProductCats(categories as Category[]); + setAllItems(products as Item[]); + setItems(products as Item[]); + setTableItems(products as Item[]); + setItemsLoaded(true); + } + ); + }, []); + + // PAGE ELEMENTS + + // FILTERING MODAL + const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); + const [filtersApplied, setFiltersApplied] = useState(false); + const [filterCategory, setFilterCategory] = useState([]); + const [filterPriceMin, setFilterPriceMin] = useState( + undefined + ); + const [filterPriceMax, setFilterPriceMax] = useState( + undefined + ); + const [filterStockMin, setFilterStockMin] = useState( + undefined + ); + const [filterStockMax, setFilterStockMax] = useState( + undefined + ); + + const filteringCriteria = { + name: { + operators: ["=", "!="], + }, + category: { + operators: ["=", "!="], + }, + price: { + operators: ["=", "!=", ">", ">=", "<", "=<"], + }, + stock: { + operators: ["=", "!=", ">", ">=", "<", "=<"], + }, + }; + + const applyFilters = () => { + console.log("Applying filters"); + console.log("Name", searchValue); + console.log("Category", filterCategory); + console.log("Price range", filterPriceMin, filterPriceMax); + console.log("Stock range", filterStockMin, filterStockMax); + + // let's find out if filters are applied + setFiltersApplied( + searchValue !== "" || + filterCategory.length > 0 || + filterPriceMin !== undefined || + filterPriceMax !== undefined || + filterStockMin !== undefined || + filterStockMax !== undefined + ); + + // filter the items + handleFiltering(); + + // close the modal + closeFilterModal(); + }; + + const clearAllFilters = (e) => { + e && e.preventDefault(); + setSearchValue(""); + setFilterCategory([]); + setFilterPriceMin(undefined); + setFilterPriceMax(undefined); + setFilterStockMin(undefined); + setFilterStockMax(undefined); + + handleFiltering(true); + setFiltersApplied(false); + }; + + const openFilterModal = () => { + setIsFilterModalOpen(true); + }; + const closeFilterModal = () => { + setIsFilterModalOpen(false); + }; + + const handleFiltering = (clear = false) => { + let filteredItems = [...allItems]; + + if (!clear) { + if (searchValue != "" && searchValue !== undefined) { + filteredItems = filteredItems.filter((item) => + item.name.toLowerCase().includes(searchValue.toLowerCase()) + ); + } + + if (filterCategory.length > 0) { + filteredItems = filteredItems.filter((item) => { + let found = false; + item.categories.forEach((cat) => { + if (filterCategory.includes(cat)) { + found = true; + } + }); + return found; + }); + } + + if (filterPriceMin !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.price >= filterPriceMin + ); + } + + if (filterPriceMax !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.price <= filterPriceMax + ); + } + + if (filterStockMin !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.stock >= filterStockMin + ); + } + + if (filterStockMax !== undefined) { + filteredItems = filteredItems.filter( + (item) => item.stock <= filterStockMax + ); + } + } + + setItems(filteredItems as Item[]); + setTableItems(filteredItems); + }; + + // Empty state + const EmptyState = ( + + {items.length < 1 && !itemsLoaded ? ( + // if products havent been loaded, let's show a loader + + ) : ( + // if products have been loaded, let's show the empty + + + { + // differentiate from empty search or empty products and show a loader element if the data is being fetched + filtersApplied + ? `No products were found for the criteria` + : "You have no products yet." + } + + + )} + + ); + + return ( + <> + navigate("/"), + href: "#", + }} + /> + } + > + + + { + //The most common way of organizing information within the BigDesign patterns is with the use of panels. + //In this case we only have one panel, but you can have multiple panels within a page. + } + + { + //search and filtering + } + + +
+ + } + /> + +
+ +
+
+ {filtersApplied && ( + + {/* let's showcase teh filters applied with chips here*/} + {searchValue && ( + setSearchValue("")} + label={`Name: ${searchValue}`} + /> + )} + {filterCategory.map((catId) => { + const cat = findCategoryById(productCats, catId); + return ( + + setFilterCategory( + filterCategory.filter((c) => c !== catId) + ) + } + label={`Category: ${cat?.label}`} + /> + ); + })} + {filterPriceMin !== undefined && ( + setFilterPriceMin(undefined)} + label={`Min price: ${filterPriceMin}`} + /> + )} + {filterPriceMax !== undefined && ( + setFilterPriceMax(undefined)} + label={`Max price: ${filterPriceMax}`} + /> + )} + {filterStockMin !== undefined && ( + setFilterStockMin(undefined)} + label={`Min stock: ${filterStockMin}`} + /> + )} + {filterStockMax !== undefined && ( + setFilterStockMax(undefined)} + label={`Max stock: ${filterStockMax}`} + /> + )} + + + Clear all filters + + + )} + + { + //The Table component is used to display tabular data. + //It allows you to display a list of items in a table format. + //The table can be customized with different columns and actions. + //The table also allows you to select items and perform actions on them. + //In this case, the table is displaying a list of products. + } + + + + + + +
+
+ + + + +
+ +
+ + + ); +}; + +export default PageFiltersAdvancedAdditive; diff --git a/packages/examples-site/src/pages/FiltersAdvancedPAge/FiltersAdvancedPage.tsx b/packages/examples-site/src/pages/FiltersAdvancedPAge/FiltersAdvancedPage.tsx index 9e87a26..4465f8c 100644 --- a/packages/examples-site/src/pages/FiltersAdvancedPAge/FiltersAdvancedPage.tsx +++ b/packages/examples-site/src/pages/FiltersAdvancedPAge/FiltersAdvancedPage.tsx @@ -49,17 +49,6 @@ import { formatPrice } from "../../helpers/price"; */ interface Item extends DummyItem, TableItem {} -/** - * Column definitions for the table. - */ - -/** - * Function to sort the items based on a column and direction. - * @param {Item[]} items - The items to sort. - * @param {string} columnHash - The column to sort by. - * @param {string} direction - The direction to sort (ASC or DESC). - * @returns {Item[]} - The sorted items. - */ const sort = (items: Item[], columnHash: string, direction: string) => { return items .concat() @@ -228,14 +217,12 @@ const PageFiltersAdvanced: FunctionComponent = () => { // set the items setItems(foundItems); setTableItems(foundItems); - setFiltersApplied(true); } }; // EFFECTS // fetch categories and product all at once - const [productCats, setProductCats] = useState([]); const [items, setItems] = useState([]); const [allItems, setAllItems] = useState([]); @@ -251,8 +238,6 @@ const PageFiltersAdvanced: FunctionComponent = () => { ); }, []); - // PAGE ELEMENTS - // FILTERING MODAL const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [filtersApplied, setFiltersApplied] = useState(false); @@ -271,16 +256,9 @@ const PageFiltersAdvanced: FunctionComponent = () => { ); const applyFilters = () => { - console.log("Applying filters"); - console.log("Name", searchValue); - console.log("Category", filterCategory); - console.log("Price range", filterPriceMin, filterPriceMax); - console.log("Stock range", filterStockMin, filterStockMax); - // let's find out if filters are applied setFiltersApplied( - searchValue !== "" || - filterCategory.length > 0 || + filterCategory.length > 0 || filterPriceMin !== undefined || filterPriceMax !== undefined || filterStockMin !== undefined || @@ -296,7 +274,6 @@ const PageFiltersAdvanced: FunctionComponent = () => { const clearAllFilters = (e) => { e && e.preventDefault(); - setSearchValue(""); setFilterCategory([]); setFilterPriceMin(undefined); setFilterPriceMax(undefined); @@ -336,25 +313,25 @@ const PageFiltersAdvanced: FunctionComponent = () => { }); } - if (filterPriceMin !== undefined) { + if (filterPriceMin !== undefined && !isNaN(filterPriceMin)) { filteredItems = filteredItems.filter( (item) => item.price >= filterPriceMin ); } - if (filterPriceMax !== undefined) { + if (filterPriceMax !== undefined && !isNaN(filterPriceMax)) { filteredItems = filteredItems.filter( (item) => item.price <= filterPriceMax ); } - if (filterStockMin !== undefined) { + if (filterStockMin !== undefined && !isNaN(filterStockMin)) { filteredItems = filteredItems.filter( (item) => item.stock >= filterStockMin ); } - if (filterStockMax !== undefined) { + if (filterStockMax !== undefined && !isNaN(filterStockMax)) { filteredItems = filteredItems.filter( (item) => item.stock <= filterStockMax ); @@ -365,6 +342,47 @@ const PageFiltersAdvanced: FunctionComponent = () => { setTableItems(filteredItems); }; + const deleteFilter = (type: string, value?: number) => { + let checkCategory = [...filterCategory]; + let checkPriceMin = filterPriceMin; + let checkPriceMax = filterPriceMax; + let checkStockMin = filterStockMin; + let checkStockMax = filterStockMax; + + switch (type) { + case "category": + checkCategory = checkCategory.filter((cat) => cat !== value); + setFilterCategory(checkCategory); + break; + case "priceMin": + setFilterPriceMin(undefined); + checkPriceMin = undefined; + break; + case "priceMax": + setFilterPriceMax(undefined); + checkPriceMax = undefined; + break; + case "stockMin": + setFilterStockMin(undefined); + checkStockMin = undefined; + break; + case "stockMax": + setFilterStockMax(undefined); + checkStockMax = undefined; + break; + default: + break; + } + + setFiltersApplied( + checkCategory.length > 0 || + checkPriceMin !== undefined || + checkPriceMax !== undefined || + checkStockMin !== undefined || + checkStockMax !== undefined + ); + }; + // Empty state const EmptyState = ( { value={searchValue} onChange={onSearchChange} iconLeft={} + type="search" /> @@ -440,54 +459,46 @@ const PageFiltersAdvanced: FunctionComponent = () => { {filtersApplied && ( - {/* let's showcase teh filters applied with chips here*/} - {searchValue && ( - setSearchValue("")} - label={`Name: ${searchValue}`} - /> - )} + {/* let's showcase the filters applied with chips here*/} {filterCategory.map((catId) => { const cat = findCategoryById(productCats, catId); return ( - setFilterCategory( - filterCategory.filter((c) => c !== catId) - ) - } + onDelete={() => { + deleteFilter("category", catId); + }} label={`Category: ${cat?.label}`} /> ); })} - {filterPriceMin !== undefined && ( + {filterPriceMin !== undefined && !isNaN(filterPriceMin) && ( setFilterPriceMin(undefined)} + onDelete={() => deleteFilter("priceMin")} label={`Min price: ${filterPriceMin}`} /> )} - {filterPriceMax !== undefined && ( + {filterPriceMax !== undefined && !isNaN(filterPriceMax) && ( setFilterPriceMax(undefined)} + onDelete={() => deleteFilter("priceMax")} label={`Max price: ${filterPriceMax}`} /> )} - {filterStockMin !== undefined && ( + {filterStockMin !== undefined && !isNaN(filterStockMin) && ( setFilterStockMin(undefined)} + onDelete={() => deleteFilter("stockMin")} label={`Min stock: ${filterStockMin}`} /> )} - {filterStockMax !== undefined && ( + {filterStockMax !== undefined && !isNaN(filterStockMax) && ( setFilterStockMax(undefined)} + onDelete={() => deleteFilter("stockMax")} label={`Max stock: ${filterStockMax}`} /> )} @@ -498,13 +509,6 @@ const PageFiltersAdvanced: FunctionComponent = () => { )} - { - //The Table component is used to display tabular data. - //It allows you to display a list of items in a table format. - //The table can be customized with different columns and actions. - //The table also allows you to select items and perform actions on them. - //In this case, the table is displaying a list of products. - }
{ onClose={closeFilterModal} isOpen={isFilterModalOpen} > - - - - setSearchValue(e.target.value)} - /> - - + {
- - - - - { onChange={(e) => setFilterPriceMin(parseInt(e.target.value)) } + placeholder="Min." /> - - - - - { onChange={(e) => setFilterPriceMax(parseInt(e.target.value)) } + placeholder="Max." /> @@ -614,35 +600,27 @@ const PageFiltersAdvanced: FunctionComponent = () => {
- - - - - setFilterStockMin(parseInt(e.target.value)) } /> - - - - - setFilterStockMax(parseInt(e.target.value)) diff --git a/packages/examples-site/src/pages/FiltersSearchPage/FiltersSearchPage.tsx b/packages/examples-site/src/pages/FiltersSearchPage/FiltersSearchPage.tsx index e629eb2..d44c2d6 100644 --- a/packages/examples-site/src/pages/FiltersSearchPage/FiltersSearchPage.tsx +++ b/packages/examples-site/src/pages/FiltersSearchPage/FiltersSearchPage.tsx @@ -153,6 +153,7 @@ const PageFiltersSearch: FunctionComponent = () => { // DATA HANDLING const [currentItems, setCurrentItems] = useState([]); + const [itemsLoaded, setItemsLoaded] = useState(false); const setTableItems = ( themItems: any, @@ -228,11 +229,11 @@ const PageFiltersSearch: FunctionComponent = () => { setAllItems(products as Item[]); setItems(products as Item[]); setTableItems(products as Item[]); + setItemsLoaded(true); } ); }, []); - // PAGE ELEMENTS // Empty state @@ -242,7 +243,7 @@ const PageFiltersSearch: FunctionComponent = () => { paddingVertical="xxxLarge" marginBottom="xxxLarge" > - {items.length < 1 ? ( + {items.length < 1 && !itemsLoaded ? ( // if products havent been loaded, let's show a loader ) : ( diff --git a/packages/examples-site/src/pages/Home/Home.tsx b/packages/examples-site/src/pages/Home/Home.tsx index ec300cd..9b43238 100644 --- a/packages/examples-site/src/pages/Home/Home.tsx +++ b/packages/examples-site/src/pages/Home/Home.tsx @@ -45,6 +45,9 @@ const PageHome: FunctionComponent = () => {
  • Advanced filtering
  • + {/*
  • + Advanced additive filtering with views +
  • */}

    Pattern Components