diff --git a/client/components/map/FacilityCard.tsx b/client/components/map/FacilityCard.tsx index abfcd7a..7779356 100644 --- a/client/components/map/FacilityCard.tsx +++ b/client/components/map/FacilityCard.tsx @@ -167,7 +167,7 @@ export const FacilityCard = ({ ); }; -const AcceptedTab: React.FC<{ children: ReactNode }> = ({ children }) => { +export const AcceptedTab: React.FC<{ children: ReactNode }> = ({ children }) => { return ( = ({ children }) => { ); }; -const UnacceptedTab: React.FC<{ children: ReactNode }> = ({ children }) => { +export const UnacceptedTab: React.FC<{ children: ReactNode }> = ({ children }) => { return ( {children} diff --git a/client/components/pickup/ButtonRow.tsx b/client/components/pickup/ButtonRow.tsx index 682f4f6..a992bb4 100644 --- a/client/components/pickup/ButtonRow.tsx +++ b/client/components/pickup/ButtonRow.tsx @@ -1,7 +1,6 @@ -import { ArrowBackIcon, ArrowLeftIcon } from "@chakra-ui/icons"; -import { Button, Flex, Heading, Spacer, Box, ButtonGroup, Icon } from "@chakra-ui/react"; +import { ArrowBackIcon } from "@chakra-ui/icons"; +import { Button, Flex, Heading, Spacer, Box, ButtonGroup } from "@chakra-ui/react"; import { Dispatch, SetStateAction } from "react"; -import { BiLeftArrow } from "react-icons/bi"; import { Pages } from "spa-pages/pageEnums"; type Props = { @@ -10,7 +9,7 @@ type Props = { const ButtonRow = ({ setPage }: Props) => { return ( - + Your items: diff --git a/client/components/pickup/ItemsAndFilterRow.tsx b/client/components/pickup/ItemsAndFilterRow.tsx index 465612a..1acdfa1 100644 --- a/client/components/pickup/ItemsAndFilterRow.tsx +++ b/client/components/pickup/ItemsAndFilterRow.tsx @@ -1,19 +1,75 @@ +import { ChangeEvent, Dispatch, SetStateAction, useState } from "react"; import { Flex, theme, useDisclosure } from "@chakra-ui/react"; import { TEmptyItem, TItemSelection } from "app-context/SheetyContext/types"; import FilterButton from "./filterPopover"; -import { SelectAndFilterBar, SelectedItemChips } from "spa-pages"; +import { SelectAndFilterBar, multiselectOnChange, OptionType, checkboxChange } from "spa-pages"; +import { ActionMeta, MultiValue } from "react-select"; +import { OrgProps } from "spa-pages"; type Props = { items: (TItemSelection | TEmptyItem)[]; + setOrgs: Dispatch>; + sortPickups: (itemEntry: (TItemSelection | TEmptyItem)[]) => OrgProps[]; }; -const ItemsAndFilterRow = ({ items }: Props) => { +const ItemsAndFilterRow = ({ items, setOrgs, sortPickups }: Props) => { const colors = theme.colors; + // Multiselect Box + const selectOptions: OptionType[] = items.map((item, index) => ({ + value: item.name, + label: item.name, + method: item.method, + idx: index, + })); + + const [itemState, setItemState] = useState<(TItemSelection | TEmptyItem)[]>(items); + const [selectedOptions, setSelectedOptions] = useState([...selectOptions]); + const { isOpen: isFilterOpen, onOpen: onFilterOpen, onClose: onFilterClose } = useDisclosure(); + const handleMultiselectOnChange = ( + newValue: MultiValue, + actionMeta: ActionMeta, + ) => { + const { updatedOptions, updatedItemState } = multiselectOnChange( + newValue, + actionMeta, + itemState, + selectedOptions, + ); + setSelectedOptions(updatedOptions); + setItemState(updatedItemState); + setOrgs(sortPickups(updatedItemState)); + }; + + // Handle changes in items selected in the Filter panel + const handleCheckboxChange = (e: ChangeEvent) => { + const { updatedItemState, updatedOptions } = checkboxChange(e, itemState, selectedOptions); + setSelectedOptions(updatedOptions); + setItemState(updatedItemState); + setOrgs(sortPickups(updatedItemState)); + }; + + const selectAllItems = () => { + const selectOptions: OptionType[] = items.map((item, index) => ({ + value: item.name, + label: item.name, + method: item.method, + idx: index, + })); + const itemState = items.map((item) => ({ + name: item.name, + method: item.method, + })); + + setItemState(itemState); + setSelectedOptions(selectOptions); + setOrgs(sortPickups(itemState)); + }; + return ( - + { borderRadius="md" > ({ - label: item.name, - value: item.name, - method: item.method, - idx, - }))} - onMultiSelectChange={() => void 0} - selectOptions={items.map((item, idx) => ({ - label: item.name, - value: item.name, - method: item.method, - idx, - }))} + selectedOptions={selectedOptions} + onMultiSelectChange={handleMultiselectOnChange} + selectOptions={selectOptions} onFilterOpen={onFilterOpen} + enableBoxShadow={false} /> - + {/* Filter Panel */} + ); }; diff --git a/client/components/pickup/OrgCard.tsx b/client/components/pickup/OrgCard.tsx index ea488f3..69fd9b7 100644 --- a/client/components/pickup/OrgCard.tsx +++ b/client/components/pickup/OrgCard.tsx @@ -1,4 +1,16 @@ -import { Text, Button, ButtonGroup, VStack, Heading, Flex, Accordion, AccordionItem, AccordionButton, Box, AccordionIcon, AccordionPanel, Spacer, theme } from "@chakra-ui/react"; +import { + Text, + Button, + ButtonGroup, + VStack, + Heading, + Flex, + Box, + theme, + Divider, +} from "@chakra-ui/react"; +import Link from "next/link"; + import { Card, CardBody } from "@chakra-ui/card"; import { TSheetyPickupDetails } from "api/sheety/types"; import { MdOutlineScale } from "react-icons/md"; @@ -6,7 +18,7 @@ import { BiTimeFive } from "react-icons/bi"; import { BsCurrencyDollar } from "react-icons/bs"; import OrgLabel from "./OrgLabel"; import { TEmptyItem, TItemSelection } from "app-context/SheetyContext/types"; -import { CheckIcon, SmallCloseIcon } from "@chakra-ui/icons"; +import { AcceptedTab, UnacceptedTab } from "components/map"; type Props = { orgDetails: TSheetyPickupDetails; @@ -24,71 +36,76 @@ const OrgCard = (props: Props) => { pricingTermsInSgd, contactMethod, contactDetail, - lastUpdated + lastUpdated, } = props.orgDetails; const { acceptedItems, notAcceptedItems } = props; const numItems = acceptedItems.length + notAcceptedItems.length; const colors = theme.colors; return ( - + - - {organisationName} - + {organisationName} - + - + - - -

- - - - Accepted: {acceptedItems.length}/{numItems} items - - - -

- - - {acceptedItems.map((item) => item.name).join(", ")} - - They also accept these items: - {categoriesAccepted.slice(0, 1)}{categoriesAccepted.slice(1).toLowerCase().replaceAll("_", " ")}. - Please check their website for more info. - - -
-
- {notAcceptedItems.length > 0 && - - -

- - - - Not Accepted: {notAcceptedItems.length}/{numItems} items - - - -

- - {notAcceptedItems.map((item) => item.name).join(", ")} - -
-
- } - + + + + They accept {acceptedItems.length} of {numItems} items: + + + {acceptedItems.map((item, idx) => ( + {item.name} + ))} + {notAcceptedItems.map((item, idx) => ( + {item.name} + ))} + + + + They also accept these items: + + {categoriesAccepted + .split(" ") + .map( + (category) => + category.slice(0, 1) + + category.slice(1).toLowerCase().replaceAll("_", " "), + ) + .join(" ")} + + + - - + diff --git a/client/components/pickup/OrgLabel.tsx b/client/components/pickup/OrgLabel.tsx index 25b173c..d83ef37 100644 --- a/client/components/pickup/OrgLabel.tsx +++ b/client/components/pickup/OrgLabel.tsx @@ -1,20 +1,25 @@ import { Text, HStack, Icon } from "@chakra-ui/react"; import { IconType } from "react-icons"; +import { COLORS } from "theme"; type Props = { - icon: IconType; - title: string; - text: string; + icon: IconType; + title: string; + text: string; }; const OrgLabel = (props: Props) => { - return ( - - - {props.title} - {props.text} - - ); + return ( + + + + {props.title} + + + {props.text} + + + ); }; -export default OrgLabel; \ No newline at end of file +export default OrgLabel; diff --git a/client/components/pickup/OrgList.tsx b/client/components/pickup/OrgList.tsx index 0d0c56a..6485bbf 100644 --- a/client/components/pickup/OrgList.tsx +++ b/client/components/pickup/OrgList.tsx @@ -1,4 +1,4 @@ -import { Divider, Heading, Center, theme } from "@chakra-ui/react"; +import { Box, Heading, Text } from "@chakra-ui/react"; import OrgCard from "./OrgCard"; import { OrgProps } from "spa-pages/components/PickupPage"; @@ -7,21 +7,25 @@ type Props = { }; const OrgList = (props: Props) => { - const colors = theme.colors; - return ( - <> -
- -
- + + Pick Up Services Near You - {props.sortedPossiblePickups.map((pickup) => ( - - ))} - + {props.sortedPossiblePickups.length > 0 ? ( + props.sortedPossiblePickups.map((pickup) => ( + + )) + ) : ( + No relevant pick up services were found. :( + )} + ); }; -export default OrgList; \ No newline at end of file +export default OrgList; diff --git a/client/components/pickup/PickupCarousel.tsx b/client/components/pickup/PickupCarousel.tsx index cf3affb..ea4c3d9 100644 --- a/client/components/pickup/PickupCarousel.tsx +++ b/client/components/pickup/PickupCarousel.tsx @@ -66,7 +66,7 @@ const PickupCarousel = ({ }; return ( - + void; - items: (TItemSelection | TEmptyItem)[]; + handleCheckboxChange: (e: ChangeEvent) => void; + selectAllItems: () => void; + itemState: (TItemSelection | TEmptyItem)[]; + selectOptions: OptionType[]; }) => { - const initRef = useRef(null); // Specify the correct type for initRef + const [modalTop, setModalTop] = useState(265); + + useEffect(() => { + const handleScroll = () => { + // Update the modal top position based on the scroll position + setModalTop(265 - window.scrollY); + }; + + // Attach the scroll event listener + window.addEventListener("scroll", handleScroll); + + // Cleanup function to remove the event listener when the component unmounts + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); const [priceValue, setPriceValue] = useState(100); const [quantityValue, setQuantityValue] = useState(0); @@ -28,30 +48,40 @@ const FilterButton = ({ setQuantityValue(val); }; + const selectedOptionsWithCheckedState = selectOptions.map((option) => { + const isChecked = itemState.some( + (item) => item.name === option.value && item.method === option.method, + ); + return { ...option, isChecked }; + }); + return ( - + + } > ({ - isChecked: true, - value: item.name, - method: item.method, - }))} - onChange={() => { - return void 0; - }} - onSelectAll={() => { - return void 0; - }} + items={selectedOptionsWithCheckedState} + onChange={handleCheckboxChange} + onSelectAll={selectAllItems} /> diff --git a/client/spa-pages/components/MapPage.tsx b/client/spa-pages/components/MapPage.tsx index 7cd1085..1c036e2 100644 --- a/client/spa-pages/components/MapPage.tsx +++ b/client/spa-pages/components/MapPage.tsx @@ -55,6 +55,7 @@ const Cluster = dynamic( ssr: false, }, ); + export type OptionType = { value: string; label: string; @@ -82,11 +83,11 @@ const MapInner = ({ setPage }: Props) => { // const [isExpanded, setIsExpanded] = useState(false); // Multiselect Box - const selectOptions: OptionType[] = items.map((item) => ({ + const selectOptions: OptionType[] = items.map((item, index) => ({ value: item.name, label: item.name, method: item.method, - idx: index++, + idx: index, })); const [selectedOptions, setSelectedOptions] = useState([...selectOptions]); // Internal tracking of user-selected items @@ -123,7 +124,6 @@ const MapInner = ({ setPage }: Props) => { ////// Variables ////// const isLoading = !map || !leafletWindow; const zoom = 15; - let index = 0; const [centerPos, setCenterPos] = useState( address.value !== "" @@ -170,6 +170,30 @@ const MapInner = ({ setPage }: Props) => { return { cardIsOpen: cardIsOpen, cardDetails: cardDetails, distance: facility.distance }; }; + const handleMultiselectOnChange = ( + newValue: MultiValue, + actionMeta: ActionMeta, + ) => { + const { updatedOptions, updatedItemState } = multiselectOnChange( + newValue, + actionMeta, + itemState, + selectedOptions, + ); + setSelectedOptions(updatedOptions); + setItemState(updatedItemState); + setFacCardIsOpen(false); + handleChangedLocation(updatedItemState); + }; + + // Handle changes in items selected in the Filter panel + const handleCheckboxChange = (e: ChangeEvent) => { + const { updatedItemState, updatedOptions } = checkboxChange(e, itemState, selectedOptions); + handleChangedLocation(updatedItemState); + setSelectedOptions(updatedOptions); + setItemState(updatedItemState); + }; + // Handle the changing of location in this page itself const handleChangedLocation = (itemEntry: (TItemSelection | TEmptyItem)[]) => { const locations = getNearbyFacilities( @@ -208,76 +232,7 @@ const MapInner = ({ setPage }: Props) => { ] as LatLngExpression); }; - // Handle change in multi-select box (remove, add items) - const handleMultiselectOnChange = ( - newValue: MultiValue, - actionMeta: ActionMeta, - ) => { - let updatedItemState: (TItemSelection | TEmptyItem)[] = itemState; - let updatedOptions: OptionType[] = selectedOptions; - // If user adds an option - if (actionMeta.action === "select-option") { - const newItem = { - name: actionMeta.option?.label, - method: actionMeta.option?.method as Methods, - } as TItemSelection; - itemState.push(newItem); - updatedItemState = [...itemState]; - updatedOptions.push(actionMeta.option as OptionType); - // If user removes an option - } else if (actionMeta.action === "remove-value") { - const removedValue = actionMeta.removedValue; - updatedItemState = itemState.filter((item) => { - return item.name !== removedValue.label; - }); - updatedOptions = selectedOptions.filter( - (option) => option.value !== removedValue.label, - ); - } - setSelectedOptions(updatedOptions); - handleChangedLocation(updatedItemState); - setItemState(updatedItemState); - setFacCardIsOpen(false); - }; - - // Handle changes in items selected in the Filter panel - const handleCheckboxChange = (e: ChangeEvent) => { - let updatedItemState: (TItemSelection | TEmptyItem)[] = itemState; - let updatedOptions: OptionType[] = selectedOptions; - if (e.target.checked) { - // If add - const newItem = { - name: e.target.value, - method: e.target.name as Methods, - } as TItemSelection; - itemState.push(newItem); - updatedItemState = [...itemState]; - const newOption: OptionType = { - value: e.target.value, - label: e.target.value, - method: e.target.name as Methods, - idx: parseInt(e.target.dataset.key as string), - }; - updatedOptions.push(newOption); - } else if (!e.target.checked) { - // If remove - updatedItemState = itemState.filter((item) => { - return item.name !== e.target.value; - }); - updatedOptions = selectedOptions.filter((option) => option.value !== e.target.value); - } - handleChangedLocation(updatedItemState); - setSelectedOptions(updatedOptions); - 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, @@ -467,13 +422,17 @@ export function SelectAndFilterBar({ selectOptions, onMultiSelectChange, onFilterOpen, -}: ComponentProps & { onFilterOpen: () => void }) { + enableBoxShadow = true, +}: ComponentProps & { + onFilterOpen: () => void; + enableBoxShadow?: boolean; +}) { return ( @@ -553,6 +512,67 @@ export function SelectedItemChips({ ); } +// Handle change in multi-select box (remove, add items) +export const multiselectOnChange = ( + newValue: MultiValue, + actionMeta: ActionMeta, + itemState: (TItemSelection | TEmptyItem)[], + selectedOptions: OptionType[], +): { updatedItemState: (TItemSelection | TEmptyItem)[]; updatedOptions: OptionType[] } => { + let updatedItemState: (TItemSelection | TEmptyItem)[] = itemState; + let updatedOptions: OptionType[] = selectedOptions; + // If user adds an option + if (actionMeta.action === "select-option") { + const newItem = { + name: actionMeta.option?.label, + method: actionMeta.option?.method as Methods, + } as TItemSelection; + itemState.push(newItem); + updatedItemState = [...itemState]; + updatedOptions.push(actionMeta.option as OptionType); + // If user removes an option + } else if (actionMeta.action === "remove-value") { + const removedValue = actionMeta.removedValue; + updatedItemState = itemState.filter((item) => { + return item.name !== removedValue.label; + }); + updatedOptions = selectedOptions.filter((option) => option.value !== removedValue.label); + } + return { updatedItemState, updatedOptions }; +}; + +export const checkboxChange = ( + e: ChangeEvent, + itemState: (TItemSelection | TEmptyItem)[], + selectedOptions: OptionType[], +): { updatedItemState: (TItemSelection | TEmptyItem)[]; updatedOptions: OptionType[] } => { + let updatedItemState: (TItemSelection | TEmptyItem)[] = itemState; + let updatedOptions: OptionType[] = selectedOptions; + if (e.target.checked) { + // If add + const newItem = { + name: e.target.value, + method: e.target.name as Methods, + } as TItemSelection; + itemState.push(newItem); + updatedItemState = [...itemState]; + const newOption: OptionType = { + value: e.target.value, + label: e.target.value, + method: e.target.name as Methods, + idx: parseInt(e.target.dataset.key as string), + }; + updatedOptions.push(newOption); + } else if (!e.target.checked) { + // If remove + updatedItemState = itemState.filter((item) => { + return item.name !== e.target.value; + }); + updatedOptions = selectedOptions.filter((option) => option.value !== e.target.value); + } + return { updatedItemState, updatedOptions }; +} + const CustomMultiValueLabel = (props: any) => { const { getItemCategory } = useSheetyData(); const category = getItemCategory(props.data.value); diff --git a/client/spa-pages/components/PickupPage.tsx b/client/spa-pages/components/PickupPage.tsx index c50126d..fe77e57 100644 --- a/client/spa-pages/components/PickupPage.tsx +++ b/client/spa-pages/components/PickupPage.tsx @@ -11,6 +11,7 @@ import { useSheetyData } from "hooks/useSheetyData"; import { TSheetyPickupDetails } from "api/sheety/types"; import { TEmptyItem, TItemSelection } from "app-context/SheetyContext/types"; import NonRecyclableModal from "components/common/NonRecyclableModal"; +import { useState } from "react"; type Props = { setPage: Dispatch>; @@ -25,8 +26,6 @@ export type OrgProps = { export const PickupPage = ({ setPage }: Props) => { const { items, recyclingLocationResults } = useUserInputs(); const results = recyclingLocationResults ? recyclingLocationResults.results : {}; - console.log(recyclingLocationResults); - // Find shortest distance to facility let minDistance = 100; if (Object.keys(results).length > 0) { @@ -43,30 +42,35 @@ export const PickupPage = ({ setPage }: Props) => { // Pick up services const { pickUpServices, getItemCategory } = useSheetyData(); - const possiblePickups = pickUpServices.filter((pickUpService) => { - let picksUpAtLeastOneItem = false; - for (const item of items) { - if (pickUpService.categoriesAccepted.includes(getItemCategory(item.name))) { - picksUpAtLeastOneItem = true; - break; + const sortPickups = (itemEntry: (TItemSelection | TEmptyItem)[]): OrgProps[] => { + const possiblePickups = pickUpServices.filter((pickUpService) => { + let picksUpAtLeastOneItem = false; + for (const item of itemEntry) { + if (pickUpService.categoriesAccepted.includes(getItemCategory(item.name))) { + picksUpAtLeastOneItem = true; + break; + } } - } - return picksUpAtLeastOneItem; - }); - const orgPropsList: OrgProps[] = possiblePickups.map((pickup) => { - return { - organisation: pickup, - acceptedItems: items.filter((item) => - pickup.categoriesAccepted.includes(getItemCategory(item.name)), - ), - notAcceptedItems: items.filter( - (item) => !pickup.categoriesAccepted.includes(getItemCategory(item.name)), - ), - }; - }); - const sortedPossiblePickups = orgPropsList.sort((a, b) => - a.acceptedItems.length > b.acceptedItems.length ? -1 : 1, - ); + return picksUpAtLeastOneItem; + }); + const orgPropsList: OrgProps[] = possiblePickups.map((pickup) => { + return { + organisation: pickup, + acceptedItems: itemEntry.filter((item) => + pickup.categoriesAccepted.includes(getItemCategory(item.name)), + ), + notAcceptedItems: itemEntry.filter( + (item) => !pickup.categoriesAccepted.includes(getItemCategory(item.name)), + ), + }; + }); + const sortedPossiblePickups = orgPropsList.sort((a, b) => + a.acceptedItems.length > b.acceptedItems.length ? -1 : 1, + ); + return sortedPossiblePickups; + }; + + const [orgs, setOrgs] = useState(sortPickups(items)); return ( @@ -79,14 +83,15 @@ export const PickupPage = ({ setPage }: Props) => { p={0} pb={5} > - - + + {/* Carousel */} + + {/* Title + Back Button */} - - + {/* Multiselect Box */} + + {/* Title + List of all Services */} +