From ef0f9b4d26e4f3fe3236b3795f0a5ca1c5f0b3c2 Mon Sep 17 00:00:00 2001
From: Zhi <neozhixuan@gmail.com>
Date: Wed, 6 Dec 2023 14:59:24 +0800
Subject: [PATCH 1/3] Feature - Base Multiselect Impleentation

---
 client/components/pickup/ButtonRow.tsx        |  5 +-
 .../components/pickup/ItemsAndFilterRow.tsx   | 52 +++++++----
 client/components/pickup/filterPopover.tsx    |  7 +-
 client/spa-pages/components/MapPage.tsx       | 87 +++++++++++--------
 client/spa-pages/components/PickupPage.tsx    | 62 ++++++-------
 5 files changed, 126 insertions(+), 87 deletions(-)

diff --git a/client/components/pickup/ButtonRow.tsx b/client/components/pickup/ButtonRow.tsx
index 682f4f6..3671e97 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 = {
diff --git a/client/components/pickup/ItemsAndFilterRow.tsx b/client/components/pickup/ItemsAndFilterRow.tsx
index 465612a..cf52203 100644
--- a/client/components/pickup/ItemsAndFilterRow.tsx
+++ b/client/components/pickup/ItemsAndFilterRow.tsx
@@ -1,17 +1,49 @@
+import { Dispatch, SetStateAction, useEffect, 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 } from "spa-pages";
+import { ActionMeta, MultiValue } from "react-select";
+import { useSheetyData } from "hooks/useSheetyData";
+import { OrgProps } from "spa-pages";
 
 type Props = {
 	items: (TItemSelection | TEmptyItem)[];
+	setOrgs: Dispatch<SetStateAction<OrgProps[]>>;
+	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<OptionType[]>([...selectOptions]);
+
 	const { isOpen: isFilterOpen, onOpen: onFilterOpen, onClose: onFilterClose } = useDisclosure();
 
+	const handleMultiselectOnChange = (
+		newValue: MultiValue<OptionType>,
+		actionMeta: ActionMeta<OptionType>,
+	) => {
+		const { updatedOptions, updatedItemState } = multiselectOnChange(
+			newValue,
+			actionMeta,
+			itemState,
+			selectedOptions,
+		);
+		setSelectedOptions(updatedOptions);
+		setItemState(updatedItemState);
+		setOrgs(sortPickups(updatedItemState));
+	};
+
 	return (
 		<Flex px={4}>
 			<Flex
@@ -22,19 +54,9 @@ const ItemsAndFilterRow = ({ items }: Props) => {
 				borderRadius="md"
 			>
 				<SelectAndFilterBar
-					selectedOptions={items.map((item, idx) => ({
-						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}
 				/>
 			</Flex>
diff --git a/client/components/pickup/filterPopover.tsx b/client/components/pickup/filterPopover.tsx
index 27e8b83..729d328 100644
--- a/client/components/pickup/filterPopover.tsx
+++ b/client/components/pickup/filterPopover.tsx
@@ -35,7 +35,12 @@ const FilterButton = ({
 				<FilterSection
 					title="Your items"
 					button={
-						<Button size="sm" color="white" bgColor={COLORS.Button.primary}>
+						<Button
+							size="sm"
+							color="white"
+							bgColor={COLORS.Button.primary}
+							onClick={onClose}
+						>
 							Apply
 						</Button>
 					}
diff --git a/client/spa-pages/components/MapPage.tsx b/client/spa-pages/components/MapPage.tsx
index 0895c56..b9f74a3 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<OptionType[]>([...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<LatLngExpression>(
 		address.value !== ""
@@ -170,6 +170,22 @@ const MapInner = ({ setPage }: Props) => {
 		return { cardIsOpen: cardIsOpen, cardDetails: cardDetails, distance: facility.distance };
 	};
 
+	const handleMultiselectOnChange = (
+		newValue: MultiValue<OptionType>,
+		actionMeta: ActionMeta<OptionType>,
+	) => {
+		const { updatedOptions, updatedItemState } = multiselectOnChange(
+			newValue,
+			actionMeta,
+			itemState,
+			selectedOptions,
+		);
+		setSelectedOptions(updatedOptions);
+		setItemState(updatedItemState);
+		setFacCardIsOpen(false);
+		handleChangedLocation(updatedItemState);
+	};
+
 	// Handle the changing of location in this page itself
 	const handleChangedLocation = (itemEntry: (TItemSelection | TEmptyItem)[]) => {
 		const locations = getNearbyFacilities(
@@ -208,38 +224,6 @@ const MapInner = ({ setPage }: Props) => {
 		] as LatLngExpression);
 	};
 
-	// Handle change in multi-select box (remove, add items)
-	const handleMultiselectOnChange = (
-		newValue: MultiValue<OptionType>,
-		actionMeta: ActionMeta<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,
-			);
-		}
-		setSelectedOptions(updatedOptions);
-		handleChangedLocation(updatedItemState);
-		setItemState(updatedItemState);
-		setFacCardIsOpen(false);
-	};
-
 	// Handle changes in items selected in the Filter panel
 	const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>) => {
 		let updatedItemState: (TItemSelection | TEmptyItem)[] = itemState;
@@ -272,11 +256,11 @@ const MapInner = ({ setPage }: Props) => {
 	};
 
 	const selectAllItems = () => {
-		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 itemState = items.map((item) => ({
 			name: item.name,
@@ -550,4 +534,33 @@ export function SelectedItemChips({
 	);
 }
 
+// Handle change in multi-select box (remove, add items)
+export const multiselectOnChange = (
+	newValue: MultiValue<OptionType>,
+	actionMeta: ActionMeta<OptionType>,
+	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 default MapPage;
diff --git a/client/spa-pages/components/PickupPage.tsx b/client/spa-pages/components/PickupPage.tsx
index c50126d..0f24ed6 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<SetStateAction<Pages>>;
@@ -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,34 @@ 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<OrgProps[]>(sortPickups(items));
 
 	return (
 		<BasePage title="Home Pickup" description="Singapore's first recycling planner">
@@ -80,13 +83,10 @@ export const PickupPage = ({ setPage }: Props) => {
 				pb={5}
 			>
 				<VStack align="stretch" my={23} spacing={4}>
-					<PickupCarousel
-						numPickupServices={sortedPossiblePickups.length}
-						minDist={minDistance}
-					/>
+					<PickupCarousel numPickupServices={orgs.length} minDist={minDistance} />
 					<ButtonRow setPage={setPage} />
-					<ItemsAndFilterRow items={items} />
-					<OrgList sortedPossiblePickups={sortedPossiblePickups} />
+					<ItemsAndFilterRow items={items} setOrgs={setOrgs} sortPickups={sortPickups} />
+					<OrgList sortedPossiblePickups={orgs} />
 				</VStack>
 			</Container>
 		</BasePage>

From 7b4fb7f934808c1b9eb2975333ef6a448ff72a6d Mon Sep 17 00:00:00 2001
From: Zhi <neozhixuan@gmail.com>
Date: Sat, 9 Dec 2023 02:34:45 +0800
Subject: [PATCH 2/3] Feature - Multiselect Functions

---
 client/api/onemap/index.ts                    |   2 +-
 client/components/map/FacilityCard.tsx        |   4 +-
 client/components/pickup/ButtonRow.tsx        |   2 +-
 .../components/pickup/ItemsAndFilterRow.tsx   |   3 +-
 client/components/pickup/OrgCard.tsx          | 121 ++++++++++--------
 client/components/pickup/OrgLabel.tsx         |  27 ++--
 client/components/pickup/OrgList.tsx          |  30 +++--
 client/components/pickup/PickupCarousel.tsx   |  10 +-
 client/components/pickup/filterPopover.tsx    |  29 ++++-
 client/spa-pages/components/MapPage.tsx       |   8 +-
 client/spa-pages/components/PickupPage.tsx    |  11 +-
 11 files changed, 156 insertions(+), 91 deletions(-)

diff --git a/client/api/onemap/index.ts b/client/api/onemap/index.ts
index 14a4049..a5cf9b0 100644
--- a/client/api/onemap/index.ts
+++ b/client/api/onemap/index.ts
@@ -3,7 +3,7 @@ import { OneMapResponse } from "./types";
 
 export const fetchAddresses = async (searchValue: string): Promise<OneMapResponse> => {
 	try {
-		const url = `https://developers.onemap.sg/commonapi/search?searchVal=${searchValue}&returnGeom=Y&getAddrDetails=Y&pageNum=1`;
+		const url = `https://www.onemap.gov.sg/api/common/elastic/search?searchVal=${searchValue}&returnGeom=Y&getAddrDetails=Y&pageNum=1`;
 		const res = await axios.get<OneMapResponse>(url, {
 			headers: {
 				Accept: "application/json",
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 (
 		<Box
 			bg={"#CCECD5"}
@@ -183,7 +183,7 @@ const AcceptedTab: React.FC<{ children: ReactNode }> = ({ children }) => {
 	);
 };
 
-const UnacceptedTab: React.FC<{ children: ReactNode }> = ({ children }) => {
+export const UnacceptedTab: React.FC<{ children: ReactNode }> = ({ children }) => {
 	return (
 		<Box bg={"#E0F0EF"} borderRadius={"42px"} minWidth={"fit-content"} padding={"5px 10px"}>
 			{children}
diff --git a/client/components/pickup/ButtonRow.tsx b/client/components/pickup/ButtonRow.tsx
index 3671e97..a992bb4 100644
--- a/client/components/pickup/ButtonRow.tsx
+++ b/client/components/pickup/ButtonRow.tsx
@@ -9,7 +9,7 @@ type Props = {
 
 const ButtonRow = ({ setPage }: Props) => {
 	return (
-		<Flex px={6}>
+		<Flex>
 			<Box>
 				<Heading size={"md"}>Your items:</Heading>
 			</Box>
diff --git a/client/components/pickup/ItemsAndFilterRow.tsx b/client/components/pickup/ItemsAndFilterRow.tsx
index cf52203..9812695 100644
--- a/client/components/pickup/ItemsAndFilterRow.tsx
+++ b/client/components/pickup/ItemsAndFilterRow.tsx
@@ -45,7 +45,7 @@ const ItemsAndFilterRow = ({ items, setOrgs, sortPickups }: Props) => {
 	};
 
 	return (
-		<Flex px={4}>
+		<Flex>
 			<Flex
 				justifyContent="space-between"
 				flexGrow={1}
@@ -58,6 +58,7 @@ const ItemsAndFilterRow = ({ items, setOrgs, sortPickups }: Props) => {
 					onMultiSelectChange={handleMultiselectOnChange}
 					selectOptions={selectOptions}
 					onFilterOpen={onFilterOpen}
+					enableBoxShadow={false}
 				/>
 			</Flex>
 			<FilterButton items={items} isOpen={isFilterOpen} onClose={onFilterClose} />
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 (
-		<Card mx={5} my={2} boxShadow="md" border="2px" borderColor={colors.gray[100]} >
+		<Card my={2} boxShadow="md" border="2px" borderColor={colors.gray[100]}>
 			<CardBody>
 				<VStack spacing={"3"} align="left">
-					<Heading size={"md"}>
-						{organisationName}
-					</Heading>
+					<Heading size={"md"}>{organisationName}</Heading>
 					<Flex wrap="wrap" columnGap={6} rowGap={1}>
-						<OrgLabel icon={MdOutlineScale} title="Min. Weight:" text={`${minimumWeightInKg} kg`} />
+						<OrgLabel
+							icon={MdOutlineScale}
+							title="Min. Weight:"
+							text={`${minimumWeightInKg} kg`}
+						/>
 						<OrgLabel icon={BiTimeFive} title="Pickup Hours:" text={time} />
-						<OrgLabel icon={BsCurrencyDollar} title="Service Cost:" text={`$${pricingTermsInSgd}`} />
+						<OrgLabel
+							icon={BsCurrencyDollar}
+							title="Service Cost:"
+							text={`$${pricingTermsInSgd}`}
+						/>
 					</Flex>
-					<Accordion allowToggle>
-						<AccordionItem border="none">
-							<h2>
-								<AccordionButton py={1} bgColor={colors.green[100]} border="1px" borderColor={colors.green[300]} borderRadius="md" _hover={{ bg: colors.green[100] }}>
-									<Box rowGap={2} as="b" flex="1" textAlign="left" fontSize="sm" textColor={colors.gray[700]}>
-										<CheckIcon boxSize="3" ml={1} mr={2} color={colors.green[400]} />
-										Accepted: {acceptedItems.length}/{numItems} items
-									</Box>
-									<AccordionIcon />
-								</AccordionButton>
-							</h2>
-							<AccordionPanel pb={4} fontSize="sm">
-								<Box>
-									<Text>{acceptedItems.map((item) => item.name).join(", ")}</Text>
-									<Spacer mt={4} />
-									<Text as="b">They also accept these items:</Text>
-									<Text>{categoriesAccepted.slice(0, 1)}{categoriesAccepted.slice(1).toLowerCase().replaceAll("_", " ")}.</Text>
-									<Text>Please check their website for more info.</Text>
-								</Box>
-							</AccordionPanel>
-						</AccordionItem>
-					</Accordion>
-					{notAcceptedItems.length > 0 &&
-						<Accordion allowToggle>
-							<AccordionItem border="none">
-								<h2>
-									<AccordionButton py={1} bgColor={colors.red[100]} border="1px" borderColor={colors.red[300]} borderRadius="md" _hover={{ bg: colors.red[100] }}>
-										<Box rowGap={2} as="b" flex="1" textAlign="left" fontSize="sm" textColor={colors.gray[700]}>
-											<SmallCloseIcon boxSize="4" mr={2} color={colors.red[400]} />
-											Not Accepted: {notAcceptedItems.length}/{numItems} items
-										</Box>
-										<AccordionIcon />
-									</AccordionButton>
-								</h2>
-								<AccordionPanel pb={4} fontSize="sm">
-									{notAcceptedItems.map((item) => item.name).join(", ")}
-								</AccordionPanel>
-							</AccordionItem>
-						</Accordion>
-					}
-					<ButtonGroup mt={4}>
+					<Divider />
+					<Box>
+						<Text color={"black"} fontWeight={500} mb={1}>
+							They accept {acceptedItems.length} of {numItems} items:
+						</Text>
+						<Flex gap={2} fontSize={"xs"} width={"100%"} wrap={"wrap"}>
+							{acceptedItems.map((item, idx) => (
+								<AcceptedTab key={idx}>{item.name}</AcceptedTab>
+							))}
+							{notAcceptedItems.map((item, idx) => (
+								<UnacceptedTab key={idx}>{item.name}</UnacceptedTab>
+							))}
+						</Flex>
+					</Box>
+					<Box>
+						<Text as="b">They also accept these items:</Text>
+						<Text>
+							{categoriesAccepted
+								.split(" ")
+								.map(
+									(category) =>
+										category.slice(0, 1) +
+										category.slice(1).toLowerCase().replaceAll("_", " "),
+								)
+								.join(" ")}
+						</Text>
+					</Box>
+					<ButtonGroup mt={2}>
 						<a href={website} target="_blank" rel="noreferrer">
-							<Button colorScheme={"teal"} variant={"outline"}>
+							<Button
+								colorScheme={"teal"}
+								variant={"outline"}
+								disabled={website ? false : true}
+							>
 								Website
 							</Button>
 						</a>
-						<a href={Number.isNaN(Number(contactDetail)) ? contactDetail : `tel:${contactDetail}`} target="_blank" rel="noreferrer">
+						<a
+							href={
+								Number.isNaN(Number(contactDetail))
+									? contactDetail
+									: `tel:${contactDetail}`
+							}
+							target="_blank"
+							rel="noreferrer"
+						>
 							<Button colorScheme={"teal"} variant={"solid"}>
 								Arrange Pickup!
 							</Button>
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 (
-        <HStack flexShrink={0}>
-            <Icon as={props.icon} boxSize={4} />
-            <Text as='b' fontSize="xs" >{props.title}</Text>
-            <Text fontSize="xs" >{props.text}</Text>
-        </HStack>
-    );
+	return (
+		<HStack flexShrink={0}>
+			<Icon as={props.icon} boxSize={4} />
+			<Text as="b" fontSize="sm" color={COLORS.teal} fontWeight={500}>
+				{props.title}
+			</Text>
+			<Text fontSize="sm" fontWeight={500}>
+				{props.text}
+			</Text>
+		</HStack>
+	);
 };
 
-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 (
-		<>
-			<Center>
-				<Divider borderColor={colors.gray[500]} w="85%" />
-			</Center>
-			<Heading size="lg" textAlign="center">
+		<Box>
+			<Heading size="md" textAlign="left" mb={4}>
 				Pick Up Services Near You
 			</Heading>
-			{props.sortedPossiblePickups.map((pickup) => (
-				<OrgCard key={pickup.organisation["s/n"]} orgDetails={pickup.organisation} acceptedItems={pickup.acceptedItems} notAcceptedItems={pickup.notAcceptedItems} />
-			))}
-		</>
+			{props.sortedPossiblePickups.length > 0 ? (
+				props.sortedPossiblePickups.map((pickup) => (
+					<OrgCard
+						key={pickup.organisation["s/n"]}
+						orgDetails={pickup.organisation}
+						acceptedItems={pickup.acceptedItems}
+						notAcceptedItems={pickup.notAcceptedItems}
+					/>
+				))
+			) : (
+				<Text textAlign={"center"}>No relevant pick up services were found. :(</Text>
+			)}
+		</Box>
 	);
 };
 
-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 ee08284..e77ed1e 100644
--- a/client/components/pickup/PickupCarousel.tsx
+++ b/client/components/pickup/PickupCarousel.tsx
@@ -8,7 +8,13 @@ import * as csstype from "csstype";
 
 const SLIDES_INTERVAL_TIME = 5000;
 
-const PickupCarousel = ({ minDist, numPickupServices }: { minDist: number, numPickupServices: number }) => {
+const PickupCarousel = ({
+	minDist,
+	numPickupServices,
+}: {
+	minDist: number;
+	numPickupServices: number;
+}) => {
 	const slides = [
 		{
 			url: useBreakpointValue({
@@ -60,7 +66,7 @@ const PickupCarousel = ({ minDist, numPickupServices }: { minDist: number, numPi
 	};
 
 	return (
-		<Box className={styles.carouselbox} px={4} h={40}>
+		<Box className={styles.carouselbox} h={40}>
 			<Carousel
 				showThumbs={false}
 				showStatus={false}
diff --git a/client/components/pickup/filterPopover.tsx b/client/components/pickup/filterPopover.tsx
index 729d328..e11101e 100644
--- a/client/components/pickup/filterPopover.tsx
+++ b/client/components/pickup/filterPopover.tsx
@@ -1,8 +1,6 @@
 import { Button, Modal, ModalContent, ModalOverlay } from "@chakra-ui/react";
-import { useState, useRef } from "react";
+import { useState, useEffect } from "react";
 import MarkedSlider from "./slider";
-import { BsFilter } from "react-icons/bs";
-import { Icon } from "@chakra-ui/icons";
 import { CheckboxGroup, FilterSection } from "components/map";
 import { COLORS } from "theme";
 import { TEmptyItem, TItemSelection } from "app-context/SheetyContext/types";
@@ -16,7 +14,22 @@ const FilterButton = ({
 	onClose: () => void;
 	items: (TItemSelection | TEmptyItem)[];
 }) => {
-	const initRef = useRef<HTMLElement | null>(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(100);
@@ -31,7 +44,13 @@ const FilterButton = ({
 	return (
 		<Modal isOpen={isOpen} onClose={onClose}>
 			<ModalOverlay />
-			<ModalContent maxWidth="calc(768px - 32px)" marginTop="330px" marginInline="4">
+			<ModalContent
+				position={"fixed"}
+				maxWidth="calc(768px - 32px)"
+				top={`${modalTop}px`}
+				marginBottom={0}
+				marginRight={3}
+			>
 				<FilterSection
 					title="Your items"
 					button={
diff --git a/client/spa-pages/components/MapPage.tsx b/client/spa-pages/components/MapPage.tsx
index b9f74a3..233737b 100644
--- a/client/spa-pages/components/MapPage.tsx
+++ b/client/spa-pages/components/MapPage.tsx
@@ -451,13 +451,17 @@ export function SelectAndFilterBar({
 	selectOptions,
 	onMultiSelectChange,
 	onFilterOpen,
-}: ComponentProps<typeof SelectedItemChips> & { onFilterOpen: () => void }) {
+	enableBoxShadow = true,
+}: ComponentProps<typeof SelectedItemChips> & {
+	onFilterOpen: () => void;
+	enableBoxShadow?: boolean;
+}) {
 	return (
 		<Flex
 			w="100%"
 			direction={"row"}
 			background="white"
-			boxShadow="2px 2px 8px 0px rgba(0, 0, 0, 0.50)"
+			boxShadow={enableBoxShadow ? "2px 2px 8px 0px rgba(0, 0, 0, 0.50)" : "none"}
 			borderRadius="6px"
 			alignItems="center"
 		>
diff --git a/client/spa-pages/components/PickupPage.tsx b/client/spa-pages/components/PickupPage.tsx
index 0f24ed6..e1168a6 100644
--- a/client/spa-pages/components/PickupPage.tsx
+++ b/client/spa-pages/components/PickupPage.tsx
@@ -42,10 +42,14 @@ export const PickupPage = ({ setPage }: Props) => {
 
 	// Pick up services
 	const { pickUpServices, getItemCategory } = useSheetyData();
+	console.log(pickUpServices);
 	const sortPickups = (itemEntry: (TItemSelection | TEmptyItem)[]): OrgProps[] => {
 		const possiblePickups = pickUpServices.filter((pickUpService) => {
 			let picksUpAtLeastOneItem = false;
 			for (const item of itemEntry) {
+				console.log("pickUpService.categoriesAccepted:", pickUpService.categoriesAccepted);
+				console.log("getItemCategory(item.name):", getItemCategory(item.name));
+
 				if (pickUpService.categoriesAccepted.includes(getItemCategory(item.name))) {
 					picksUpAtLeastOneItem = true;
 					break;
@@ -69,6 +73,7 @@ export const PickupPage = ({ setPage }: Props) => {
 		);
 		return sortedPossiblePickups;
 	};
+
 	const [orgs, setOrgs] = useState<OrgProps[]>(sortPickups(items));
 
 	return (
@@ -82,10 +87,14 @@ export const PickupPage = ({ setPage }: Props) => {
 				p={0}
 				pb={5}
 			>
-				<VStack align="stretch" my={23} spacing={4}>
+				<VStack align="stretch" my={23} spacing={4} px={6}>
+					{/* Carousel */}
 					<PickupCarousel numPickupServices={orgs.length} minDist={minDistance} />
+					{/* Title + Back Button */}
 					<ButtonRow setPage={setPage} />
+					{/* Multiselect Box */}
 					<ItemsAndFilterRow items={items} setOrgs={setOrgs} sortPickups={sortPickups} />
+					{/* Title + List of all Services */}
 					<OrgList sortedPossiblePickups={orgs} />
 				</VStack>
 			</Container>

From 38a25554c6d41cd5e6f19353f2c660eaa05b69a6 Mon Sep 17 00:00:00 2001
From: Zhi <neozhixuan@gmail.com>
Date: Sat, 9 Dec 2023 10:32:28 +0800
Subject: [PATCH 3/3] Feature - Working Panel (not the filters)

---
 .../components/pickup/ItemsAndFilterRow.tsx   | 40 +++++++++-
 client/components/pickup/filterPopover.tsx    | 34 ++++----
 client/spa-pages/components/MapPage.tsx       | 77 ++++++++++---------
 client/spa-pages/components/PickupPage.tsx    |  4 -
 4 files changed, 96 insertions(+), 59 deletions(-)

diff --git a/client/components/pickup/ItemsAndFilterRow.tsx b/client/components/pickup/ItemsAndFilterRow.tsx
index 9812695..1acdfa1 100644
--- a/client/components/pickup/ItemsAndFilterRow.tsx
+++ b/client/components/pickup/ItemsAndFilterRow.tsx
@@ -1,10 +1,9 @@
-import { Dispatch, SetStateAction, useEffect, useState } from "react";
+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, multiselectOnChange, OptionType } from "spa-pages";
+import { SelectAndFilterBar, multiselectOnChange, OptionType, checkboxChange } from "spa-pages";
 import { ActionMeta, MultiValue } from "react-select";
-import { useSheetyData } from "hooks/useSheetyData";
 import { OrgProps } from "spa-pages";
 
 type Props = {
@@ -44,6 +43,31 @@ const ItemsAndFilterRow = ({ items, setOrgs, sortPickups }: Props) => {
 		setOrgs(sortPickups(updatedItemState));
 	};
 
+	// Handle changes in items selected in the Filter panel
+	const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>) => {
+		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 (
 		<Flex>
 			<Flex
@@ -61,7 +85,15 @@ const ItemsAndFilterRow = ({ items, setOrgs, sortPickups }: Props) => {
 					enableBoxShadow={false}
 				/>
 			</Flex>
-			<FilterButton items={items} isOpen={isFilterOpen} onClose={onFilterClose} />
+			{/* Filter Panel */}
+			<FilterButton
+				isOpen={isFilterOpen}
+				onClose={onFilterClose}
+				handleCheckboxChange={handleCheckboxChange}
+				selectAllItems={selectAllItems}
+				itemState={itemState}
+				selectOptions={selectOptions}
+			/>
 		</Flex>
 	);
 };
diff --git a/client/components/pickup/filterPopover.tsx b/client/components/pickup/filterPopover.tsx
index e11101e..e239d96 100644
--- a/client/components/pickup/filterPopover.tsx
+++ b/client/components/pickup/filterPopover.tsx
@@ -1,18 +1,25 @@
 import { Button, Modal, ModalContent, ModalOverlay } from "@chakra-ui/react";
-import { useState, useEffect } from "react";
+import { useState, useEffect, ChangeEvent } from "react";
 import MarkedSlider from "./slider";
 import { CheckboxGroup, FilterSection } from "components/map";
 import { COLORS } from "theme";
 import { TEmptyItem, TItemSelection } from "app-context/SheetyContext/types";
+import { OptionType } from "spa-pages";
 
 const FilterButton = ({
 	isOpen,
 	onClose,
-	items,
+	handleCheckboxChange,
+	selectAllItems,
+	selectOptions,
+	itemState,
 }: {
 	isOpen: boolean;
 	onClose: () => void;
-	items: (TItemSelection | TEmptyItem)[];
+	handleCheckboxChange: (e: ChangeEvent<HTMLInputElement>) => void;
+	selectAllItems: () => void;
+	itemState: (TItemSelection | TEmptyItem)[];
+	selectOptions: OptionType[];
 }) => {
 	const [modalTop, setModalTop] = useState(265);
 
@@ -41,6 +48,13 @@ 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 (
 		<Modal isOpen={isOpen} onClose={onClose}>
 			<ModalOverlay />
@@ -65,17 +79,9 @@ const FilterButton = ({
 					}
 				>
 					<CheckboxGroup
-						items={items.map((item) => ({
-							isChecked: true,
-							value: item.name,
-							method: item.method,
-						}))}
-						onChange={() => {
-							return void 0;
-						}}
-						onSelectAll={() => {
-							return void 0;
-						}}
+						items={selectedOptionsWithCheckedState}
+						onChange={handleCheckboxChange}
+						onSelectAll={selectAllItems}
 					/>
 				</FilterSection>
 
diff --git a/client/spa-pages/components/MapPage.tsx b/client/spa-pages/components/MapPage.tsx
index 233737b..5029d40 100644
--- a/client/spa-pages/components/MapPage.tsx
+++ b/client/spa-pages/components/MapPage.tsx
@@ -186,6 +186,14 @@ const MapInner = ({ setPage }: Props) => {
 		handleChangedLocation(updatedItemState);
 	};
 
+	// Handle changes in items selected in the Filter panel
+	const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>) => {
+		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(
@@ -224,44 +232,7 @@ const MapInner = ({ setPage }: Props) => {
 		] as LatLngExpression);
 	};
 
-	// Handle changes in items selected in the Filter panel
-	const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>) => {
-		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, index) => ({
-			value: item.name,
-			label: item.name,
-			method: item.method,
-			idx: index,
-		}));
 		const itemState = items.map((item) => ({
 			name: item.name,
 			method: item.method,
@@ -567,4 +538,36 @@ export const multiselectOnChange = (
 	return { updatedItemState, updatedOptions };
 };
 
+export const checkboxChange = (
+	e: ChangeEvent<HTMLInputElement>,
+	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 };
+};
+
 export default MapPage;
diff --git a/client/spa-pages/components/PickupPage.tsx b/client/spa-pages/components/PickupPage.tsx
index e1168a6..fe77e57 100644
--- a/client/spa-pages/components/PickupPage.tsx
+++ b/client/spa-pages/components/PickupPage.tsx
@@ -42,14 +42,10 @@ export const PickupPage = ({ setPage }: Props) => {
 
 	// Pick up services
 	const { pickUpServices, getItemCategory } = useSheetyData();
-	console.log(pickUpServices);
 	const sortPickups = (itemEntry: (TItemSelection | TEmptyItem)[]): OrgProps[] => {
 		const possiblePickups = pickUpServices.filter((pickUpService) => {
 			let picksUpAtLeastOneItem = false;
 			for (const item of itemEntry) {
-				console.log("pickUpService.categoriesAccepted:", pickUpService.categoriesAccepted);
-				console.log("getItemCategory(item.name):", getItemCategory(item.name));
-
 				if (pickUpService.categoriesAccepted.includes(getItemCategory(item.name))) {
 					picksUpAtLeastOneItem = true;
 					break;