diff --git a/client/components/map/FilterPanel.tsx b/client/components/map/FilterPanel.tsx index 3b7bf5d..e4f8ac1 100644 --- a/client/components/map/FilterPanel.tsx +++ b/client/components/map/FilterPanel.tsx @@ -1,117 +1,245 @@ import { Box, - VStack, HStack, - Spacer, - Button, Text, Slider, SliderTrack, SliderFilledTrack, SliderThumb, + Modal, + ModalContent, + ModalOverlay, Divider, - CheckboxGroup, - Checkbox, - Stack, + Button, + Flex, + useCheckbox, + chakra, + CheckboxProps, + useCheckboxGroup, + Spacer, + useRadio, + UseRadioProps, + useRadioGroup, } from "@chakra-ui/react"; -import { XButton } from "./Buttons"; import { COLORS } from "theme"; import { TItemSelection, TEmptyItem } from "app-context/SheetyContext/types"; import { OptionType } from "spa-pages"; -import { ChangeEvent } from "react"; +import React, { ChangeEvent, PropsWithChildren } from "react"; +import { Methods } from "api/sheety/enums"; type FilterProps = { - isMobile: boolean | undefined; - setFilterShow: () => void; + isOpen: boolean; filterApply: () => void; handleSliderChange: (val: number) => void; range: number; itemState: (TItemSelection | TEmptyItem)[]; selectOptions: OptionType[]; handleCheckboxChange: (e: ChangeEvent<HTMLInputElement>) => void; + selectAllItems: () => void; }; export const FilterPanel = ({ - isMobile, - setFilterShow, + isOpen, filterApply, handleSliderChange, range, itemState, selectOptions, handleCheckboxChange, + selectAllItems, }: FilterProps) => { + const selectedOptionsWithCheckedState = selectOptions.map((option) => { + const isChecked = itemState.some( + (item) => item.name === option.value && item.method === option.method, + ); + return { ...option, isChecked }; + }); + return ( - <Box - position={"fixed"} - height={isMobile ? "calc(92vh)" : "calc(90vh)"} - width={"100vw"} - bg="white" - zIndex={99999} - bottom={0} - overflowY={"scroll"} - overflowX={"hidden"} - > - <VStack p={7} w={"100vw"} gap={5}> - <HStack w="100%" justify="space-between"> - <XButton onClick={setFilterShow} /> - <Spacer /> - <Button - color={COLORS.white} - bgColor={COLORS.Button.primary} - onClick={filterApply} - > - Apply - </Button> - </HStack> - - <VStack w="100%"> - <Text w="100%" textAlign={"left"} fontWeight={"bold"}> - Distance - </Text> - <HStack w="80%" justify="space-between"> + <Modal isOpen={isOpen} onClose={() => undefined}> + <ModalOverlay /> + <ModalContent maxWidth="calc(768px - 32px)" marginTop="140px" marginInline="4"> + <FilterSection + title="Your items" + button={ + <Button + size="sm" + color="white" + bgColor={COLORS.Button.primary} + onClick={filterApply} + > + Apply + </Button> + } + > + <CheckboxGroup + items={selectedOptionsWithCheckedState} + onChange={handleCheckboxChange} + onSelectAll={selectAllItems} + /> + </FilterSection> + + <FilterSection title="Sort by"> + <ChipRadioGroup items={["Nearest", "Most items"]} /> + </FilterSection> + + <FilterSection title="Max Distance" hideDivider={true}> + <HStack justify="space-between"> <Text>3km</Text> <Text>10km</Text> </HStack> <Slider min={30} max={100} - w="80%" aria-label="slider-ex-1" defaultValue={range} onChangeEnd={(val) => handleSliderChange(val)} > <SliderTrack> - <SliderFilledTrack /> + <SliderFilledTrack background="#31979566" /> </SliderTrack> - <SliderThumb /> + <SliderThumb background={COLORS.Button.primary} /> </Slider> - </VStack> - <Divider borderColor="gray.500" borderWidth="1px" /> - <VStack align="flex-start" w="100%"> - <Text textAlign={"left"} fontWeight={"bold"}> - Items - </Text> - <CheckboxGroup - colorScheme="blue" - defaultValue={itemState.map((item) => item.name)} - > - <Stack pl={1} spacing={3} direction={"column"}> - {selectOptions.map((item) => ( - <Checkbox - onChange={(e) => handleCheckboxChange(e)} - key={item.idx} - data-key={item.idx} - value={item.value} - name={item.method} - > - {item.value} - </Checkbox> - ))} - </Stack> - </CheckboxGroup> - </VStack> - </VStack> + </FilterSection> + </ModalContent> + </Modal> + ); +}; + +function FilterSection({ + title, + hideDivider, + button, + children, +}: React.PropsWithChildren<{ title: string; hideDivider?: boolean; button?: JSX.Element }>) { + return ( + <Box w="100%" p={2} pb={0}> + <HStack justify="space-between" mb={3}> + <Text fontWeight={"bold"}>{title}</Text> + {button} + </HStack> + {children} + {hideDivider ? ( + <Spacer mb={4} /> + ) : ( + <Divider background="gray.200" h="1px" marginBlock={4} /> + )} + </Box> + ); +} + +const CheckboxGroup = ({ + items, + onChange, + onSelectAll, +}: { + items: Array<{ + value: string; + method?: Methods; + isChecked: boolean; + }>; + onChange: (item: any) => void; + onSelectAll: () => void; +}) => { + const { getCheckboxProps } = useCheckboxGroup({ + value: items.filter((item) => item.isChecked).map((item) => item.value), + }); + + return ( + <Flex flex={1} gap={2} flexWrap="wrap" maxH="70px" overflow="auto" alignItems="center"> + {items.map((item) => ( + <ChipCheckbox + key={item.value} + {...getCheckboxProps({ + value: item.value, + name: item.method, + })} + onChange={onChange} + /> + ))} + <Text + color="black" + fontSize="12px" + fontWeight="bold" + cursor="pointer" + onClick={onSelectAll} + > + Select All + </Text> + </Flex> + ); +}; + +const ChipCheckbox = (props: CheckboxProps) => { + const { state, getInputProps, htmlProps } = useCheckbox(props); + + return ( + <chakra.label {...htmlProps}> + <input {...getInputProps()} hidden /> + <Chip isChecked={state.isChecked}>{props.value}</Chip> + </chakra.label> + ); +}; + +const ChipRadioGroup = ({ items }: { items: Array<string> }) => { + const { getRootProps, getRadioProps } = useRadioGroup({ + name: "sortBy", + defaultValue: "Nearest", + }); + + const group = getRootProps(); + + return ( + <HStack {...group}> + {items.map((value) => { + const radio = getRadioProps({ value }); + return ( + <ChipRadio key={value} {...radio}> + {value} + </ChipRadio> + ); + })} + </HStack> + ); +}; + +const ChipRadio = (props: PropsWithChildren<UseRadioProps>) => { + const { getInputProps, getRadioProps, state } = useRadio(props); + + const input = getInputProps(); + + return ( + <Box as="label"> + <input {...input} /> + <Chip isChecked={state.isChecked} darkBackground={true}> + {props.children} + </Chip> </Box> ); }; + +const Chip = ({ + children, + isChecked, + darkBackground, +}: React.PropsWithChildren<{ isChecked: boolean; darkBackground?: boolean }>) => { + const selectedColor = darkBackground ? "teal.500" : "teal.50"; + const selectedTextColor = darkBackground ? "white" : "black"; + + return ( + <Text + bg="white" + borderRadius="full" + paddingX="10px" + paddingY="5px" + background={isChecked ? selectedColor : "white"} + color={selectedTextColor && isChecked ? selectedTextColor : "black"} + border="1px solid #D6EAEA" + cursor="pointer" + whiteSpace="nowrap" + fontSize="12px" + > + {children} + </Text> + ); +}; diff --git a/client/spa-pages/components/MapPage.tsx b/client/spa-pages/components/MapPage.tsx index 579620e..160e371 100644 --- a/client/spa-pages/components/MapPage.tsx +++ b/client/spa-pages/components/MapPage.tsx @@ -1,6 +1,6 @@ // General Imports import { BasePage } from "layouts/BasePage"; -import { Flex, VStack, Box, IconButton } from "@chakra-ui/react"; +import { Flex, VStack, Box, IconButton, useDisclosure } from "@chakra-ui/react"; import { Dispatch, SetStateAction, useState } from "react"; import { Pages } from "spa-pages/pageEnums"; import { useUserInputs } from "hooks/useUserSelection"; @@ -78,7 +78,7 @@ const MapInner = ({ setPage }: Props) => { ////// States ////// // Filters - const [filterShow, setFilterShow] = useState(false); + const { isOpen: isFilterOpen, onOpen: onFilterOpen, onClose: onFilterClose } = useDisclosure(); const [range, setRange] = useState(60); // const [isExpanded, setIsExpanded] = useState(false); @@ -279,6 +279,23 @@ const MapInner = ({ setPage }: Props) => { setItemState(updatedItemState); }; + const selectAllItems = () => { + const selectOptions: OptionType[] = items.map((item) => ({ + value: item.name, + label: item.name, + method: item.method, + idx: index++, + })); + const itemState = items.map((item) => ({ + name: item.name, + method: item.method, + })); + + handleChangedLocation(itemState); + setItemState(itemState); + setSelectedOptions(selectOptions); + }; + // Handle the changes in distance selected in Filter panel const handleSliderChange = (val: number) => { const dist = val / 10; @@ -353,7 +370,7 @@ const MapInner = ({ setPage }: Props) => { <MapHeaderButtons setPage={setPage} - setFilterShow={setFilterShow} + onFilterOpen={onFilterOpen} selectedOptions={selectedOptions} selectOptions={selectOptions} handleMultiselectOnChange={handleMultiselectOnChange} @@ -398,18 +415,16 @@ const MapInner = ({ setPage }: Props) => { getMatchingFacility={getMatchingFacility} /> )} */} - {filterShow && ( - <FilterPanel - isMobile={isMobile} - setFilterShow={() => setFilterShow(true)} - filterApply={() => setFilterShow(false)} - handleSliderChange={handleSliderChange} - range={range} - itemState={itemState} - selectOptions={selectOptions} - handleCheckboxChange={handleCheckboxChange} - /> - )} + <FilterPanel + isOpen={isFilterOpen} + filterApply={() => onFilterClose()} + handleSliderChange={handleSliderChange} + range={range} + itemState={itemState} + selectOptions={selectOptions} + selectAllItems={selectAllItems} + handleCheckboxChange={handleCheckboxChange} + /> </BasePage> ); }; @@ -423,15 +438,14 @@ export const MapPage = ({ setPage }: Props) => ( export const MapHeaderButtons = ({ setPage, - setFilterShow, selectedOptions, selectOptions, handleMultiselectOnChange, itemState, handleChangedLocation, + onFilterOpen, }: { setPage: Dispatch<SetStateAction<Pages>>; - setFilterShow: Dispatch<SetStateAction<boolean>>; selectedOptions: OptionType[]; selectOptions: OptionType[]; handleMultiselectOnChange: ( @@ -440,6 +454,7 @@ export const MapHeaderButtons = ({ ) => void; itemState: (TItemSelection | TEmptyItem)[]; handleChangedLocation: (itemEntry: (TItemSelection | TEmptyItem)[]) => void; + onFilterOpen: () => void; }) => { return ( <VStack @@ -487,7 +502,7 @@ export const MapHeaderButtons = ({ handleMultiselectOnChange={handleMultiselectOnChange} selectOptions={selectOptions} /> - <FilterButton onClick={() => setFilterShow(true)} height="44px" /> + <FilterButton onClick={onFilterOpen} height="44px" /> </Flex> </VStack> );