diff --git a/app/seasonal-planting-guide/page.tsx b/app/seasonal-planting-guide/page.tsx index a613ea5..fce3bdb 100644 --- a/app/seasonal-planting-guide/page.tsx +++ b/app/seasonal-planting-guide/page.tsx @@ -5,8 +5,9 @@ import FilterDropdownMultiple from '@/components/FilterDropdownMultiple'; import FilterDropdownSingle from '@/components/FilterDropdownSingle'; import { PlantCalendarList } from '@/components/PlantCalendarList'; import SearchBar from '@/components/SearchBar'; +import SeasonColorKey from '@/components/SeasonColorKey'; import COLORS from '@/styles/colors'; -import { Box } from '@/styles/containers'; +import { Box, Flex } from '@/styles/containers'; import { H1, H3 } from '@/styles/text'; import { DropdownOption, PlantingTypeEnum, SeasonEnum } from '@/types/schema'; import { useProfile } from '@/utils/ProfileProvider'; @@ -16,6 +17,7 @@ import { PageContainer, PageTitle, StateOptionsContainer, + VerticalSeparator, } from './styles'; // Declaring (static) filter options outside so they're not re-rendered @@ -36,7 +38,7 @@ const plantingTypeOptions: DropdownOption[] = [ { label: 'Start Seeds Indoors', value: 'INDOORS' }, { label: 'Start Seeds Outdoors', value: 'OUTDOORS' }, { - label: 'Plant Seedlings/Transplant Outdoors', + label: 'Plant Seedlings / Transplant Outdoors', value: 'TRANSPLANT', }, ]; @@ -56,7 +58,8 @@ export default function SeasonalPlantingGuide() { const [selectedPlantingType, setSelectedPlantingType] = useState< DropdownOption[] >([]); - const [selectedUsState, setSelectedUsState] = useState(''); + const [selectedUsState, setSelectedUsState] = + useState | null>(null); const [searchTerm, setSearchTerm] = useState(''); const clearFilters = () => { @@ -67,7 +70,12 @@ export default function SeasonalPlantingGuide() { useEffect(() => { if (profileReady && profileData) { - setSelectedUsState(profileData.us_state); + setSelectedUsState({ + label: + profileData.us_state.charAt(0) + + profileData.us_state.slice(1).toLowerCase(), // can't use useTitleCase here, lint error + value: profileData.us_state, + }); } }, [profileData, profileReady]); @@ -82,14 +90,17 @@ export default function SeasonalPlantingGuide() { + {/* vertical bar to separate state and other filters */} + +

Choose Your State

) : ( - + + + + (''); const [selectedPlants, setSelectedPlants] = useState([]); const [ownedPlants, setOwnedPlants] = useState([]); + const [isCardKeyOpen, setIsCardKeyOpen] = useState(false); + const cardKeyRef = useRef(null); + const infoButtonRef = useRef(null); const userState = profileData?.us_state ?? null; const profileAndAuthReady = profileReady && !authLoading; @@ -319,12 +325,52 @@ export default function Page() { const plantPluralityString = selectedPlants.length > 1 ? 'Plants' : 'Plant'; + // close plant card key when clicking outside, even on info button + const handleClickOutside = (event: MouseEvent) => { + if ( + cardKeyRef.current && + !cardKeyRef.current.contains(event.target as Node) && + infoButtonRef.current && + !infoButtonRef.current.contains(event.target as Node) + ) { + setIsCardKeyOpen(false); + } + }; + + // handle clicking outside PlantCardKey to close it if open + useEffect(() => { + if (isCardKeyOpen) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isCardKeyOpen]); + return (
-

- View Plants -

+ +

+ View Plants +

+
+ setIsCardKeyOpen(!isCardKeyOpen)} + ref={infoButtonRef} + > + + + {isCardKeyOpen && ( +
+ +
+ )} +
+
{ value: DropdownOption[]; @@ -17,16 +25,84 @@ export default function FilterDropdownMultiple({ placeholder, disabled = false, }: FilterDropdownProps) { + const handleChange = (selectedOptions: MultiValue>) => { + setStateAction(selectedOptions as DropdownOption[]); + }; + + // overrides the default MultiValue to display custom text + // displays first selected value followed by + n if more than 1 selected + // StyledMultiValue appears for each selected option, so if more than 1 is selected, + // the rest of the selected options are not shown, instead the + n is shown as part of the first option + const StyledMultiValue = ({ + ...props + }: MultiValueProps< + DropdownOption, + true, + GroupBase> + >) => { + const { selectProps, data } = props; + if (Array.isArray(selectProps.value)) { + // find index of the selected option and check if its the first + const index = selectProps.value.findIndex( + (option: DropdownOption) => option.value === data.value, + ); + const isFirst = index === 0; + // find number of remaining selected options + const additionalCount = selectProps.value.length - 1; + + return ( + + {/* display label of first selected option */} + {isFirst ? ( + <> + {data.label} + {/* display additional count only if more than one option is selected*/} + {additionalCount > 0 && ` +${additionalCount}`} + + ) : // don't display anything if not the first selected option + null} + + ); + } + + // nothing is selected yet + return null; + }; + + // overrides the default Options to display a checkbox that ticks when option selected + const CustomOption = ( + props: OptionProps, true, GroupBase>>, + ) => { + return ( + + + null} //no-op + style={{ marginRight: 8 }} // spacing between checkbox and text + /> + {props.label} + + + ); + }; + return ( - ()} + isSearchable={false} + hideSelectedOptions={false} + // use custom styled components instead of default components + components={{ MultiValue: StyledMultiValue, Option: CustomOption }} + menuPosition="fixed" /> ); } diff --git a/components/FilterDropdownMultiple/styles.ts b/components/FilterDropdownMultiple/styles.ts index 82284ae..316d6eb 100644 --- a/components/FilterDropdownMultiple/styles.ts +++ b/components/FilterDropdownMultiple/styles.ts @@ -1,24 +1,73 @@ -import { MultiSelect } from 'react-multi-select-component'; +import { StylesConfig } from 'react-select'; import styled from 'styled-components'; +import COLORS from '@/styles/colors'; +import { DropdownOption } from '@/types/schema'; -export const StyledMultiSelect = styled(MultiSelect)` - .dropdown-container { - border-radius: 60px; !important - padding: 8px 14px 8px 14px; !important - align-items: center; - justify-content: center; - justify-items: center; - gap: 2px; - background-color: #1f5a2a; !important - border: 0.5px solid #888; !important - color: #fff; - position: relative; - } - - .dropdown-content { - display: block; !important - position: absolute; !important - z-index: 10000; !important - top: 100%; - } +export const StyledOption = styled.div` + display: flex; + align-items: center; `; + +// custom styles for react-select component +// Option type is DropdownOption and isMulti is true +export const customSelectStyles = (): StylesConfig< + DropdownOption, + true +> => ({ + // container + control: (baseStyles, state) => ({ + ...baseStyles, + borderRadius: '57px', + border: `0.5px solid ${COLORS.midgray}`, + backgroundColor: state.isDisabled ? COLORS.lightgray : '#fff', + padding: '8px 14px', + color: COLORS.midgray, + minWidth: '138px', + }), + // placeholder text + placeholder: baseStyles => ({ + ...baseStyles, + color: COLORS.midgray, + fontSize: '0.75rem', + padding: '0px', + margin: '0px', + }), + // hide vertical bar between arrow and text + indicatorSeparator: baseStyles => ({ + ...baseStyles, + display: 'none', + }), + // 'x' to clear selected option(s) + clearIndicator: baseStyles => ({ + ...baseStyles, + padding: '0px', + }), + // dropdown arrow + dropdownIndicator: baseStyles => ({ + ...baseStyles, + padding: '0px', + marginLeft: '-4px', // move the dropdown indicator to the left, cant override text styles + color: COLORS.midgray, + }), + // container for selected multi option + multiValue: baseStyles => ({ + ...baseStyles, + backgroundColor: '#fff', + border: '0px', + padding: '0px', + margin: '0px', + }), + // multi option display text + multiValueLabel: baseStyles => ({ + ...baseStyles, + fontSize: '0.75rem', + color: `${COLORS.black} !important`, + padding: '0px', + paddingLeft: '0px', + }), + // hide 'x' next to each multi option + multiValueRemove: baseStyles => ({ + ...baseStyles, + display: 'none', + }), +}); diff --git a/components/FilterDropdownSingle/index.tsx b/components/FilterDropdownSingle/index.tsx index 261fecb..492f2b5 100644 --- a/components/FilterDropdownSingle/index.tsx +++ b/components/FilterDropdownSingle/index.tsx @@ -1,57 +1,51 @@ -import React, { useState } from 'react'; +import React from 'react'; +import Select, { MultiValue, SingleValue } from 'react-select'; import { DropdownOption } from '@/types/schema'; -import { FilterDropdownInput } from './styles'; +import { customSelectStyles } from './styles'; -interface FilterDropdownProps { - name?: string; - id?: string; - value: string; - setStateAction: React.Dispatch>; - options: DropdownOption[]; +interface FilterDropdownProps { + value: DropdownOption | null; + setStateAction: React.Dispatch< + React.SetStateAction | null> + >; + options: DropdownOption[]; placeholder: string; disabled?: boolean; + // for custom styling since initial dropdown to select user's state + // is a different size to a normal single dropdown + small?: boolean; } -export default function FilterDropdownSingle({ - name, - id, +export default function FilterDropdownSingle({ value, setStateAction, options, placeholder, disabled, -}: FilterDropdownProps) { - const [isOpen, setIsOpen] = useState(false); - - const handleChange = (event: React.ChangeEvent) => { - setStateAction(event.target.value); - setIsOpen(false); - }; - - const handleToggle = () => { - setIsOpen(!isOpen); + small = false, +}: FilterDropdownProps) { + const handleChange = ( + selectedOptions: + | SingleValue> + | MultiValue>, + ) => { + if (!Array.isArray(selectedOptions)) { + setStateAction(selectedOptions as DropdownOption); + } }; return ( - setIsOpen(false)} +