diff --git a/app/seasonal-planting-guide/page.tsx b/app/seasonal-planting-guide/page.tsx index 7f54faa..b9acb65 100644 --- a/app/seasonal-planting-guide/page.tsx +++ b/app/seasonal-planting-guide/page.tsx @@ -1,68 +1,126 @@ 'use client'; import React, { useState } from 'react'; -import FilterDropdown from '@/components/FilterDropdown'; +import FilterDropdownMultiple from '@/components/FilterDropdownMultiple'; +import FilterDropdownSingle from '@/components/FilterDropdownSingle'; import { PlantList } from '@/components/PlantList'; +import SearchBar from '@/components/SearchBar'; +import { DropdownOption } from '@/types/schema'; +import { + FilterContainer, + HeaderContainer, + PageContainer, + StateOptionsContainer, +} from './styles'; -const SeasonalPlantingGuide = () => { - const growingSeasonOptions = ['Spring', 'Summer', 'Fall', 'Winter']; - const harvestSeasonOptions = ['Spring', 'Summer', 'Fall', 'Winter']; - const plantingTypeOptions = [ - 'Start Seeds Indoors', - 'Start Seeds Outdoors', - 'Plant Seedlings/Transplant Outdoors', +export default function SeasonalPlantingGuide() { + const growingSeasonOptions: DropdownOption[] = [ + { label: 'Spring', value: 'SPRING' }, + { label: 'Summer', value: 'SUMMER' }, + { label: 'Fall', value: 'FALL' }, + { label: 'Winter', value: 'WINTER' }, + ]; + const harvestSeasonOptions: DropdownOption[] = [ + { label: 'Spring', value: 'SPRING' }, + { label: 'Summer', value: 'SUMMER' }, + { label: 'Fall', value: 'FALL' }, + { label: 'Winter', value: 'WINTER' }, + ]; + const plantingTypeOptions: DropdownOption[] = [ + { label: 'Start Seeds Indoors', value: 'Start Seeds Indoors' }, + { label: 'Start Seeds Outdoors', value: 'Start Seeds Outdoors' }, + { + label: 'Plant Seedlings/Transplant Outdoors', + value: 'Plant Seedlings/Transplant Outdoors', + }, + ]; + const usStateOptions: DropdownOption[] = [ + { label: 'Tennessee', value: 'TENNESSEE' }, + { label: 'Missouri', value: 'MISSOURI' }, ]; - const [selectedGrowingSeason, setSelectedGrowingSeason] = - useState(''); - const [selectedHarvestSeason, setSelectedHarvestSeason] = - useState(''); - const [selectedPlantingType, setSelectedPlantingType] = useState(''); + const [selectedGrowingSeason, setSelectedGrowingSeason] = useState< + DropdownOption[] + >([]); + const [selectedHarvestSeason, setSelectedHarvestSeason] = useState< + DropdownOption[] + >([]); + const [selectedPlantingType, setSelectedPlantingType] = useState< + DropdownOption[] + >([]); + const [selectedUsState, setSelectedUsState] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); const clearFilters = () => { - setSelectedGrowingSeason(''); - setSelectedHarvestSeason(''); - setSelectedPlantingType(''); + setSelectedGrowingSeason([]); + setSelectedHarvestSeason([]); + setSelectedPlantingType([]); }; return ( -
- + + {!selectedUsState ? ( + <> +

Please select a US state to view planting information.

+ + + + + ) : ( + <> + + + + - + - + - + - -
- ); -}; + + + -export default SeasonalPlantingGuide; + + + )} + + ); +} diff --git a/app/seasonal-planting-guide/styles.ts b/app/seasonal-planting-guide/styles.ts new file mode 100644 index 0000000..19e1592 --- /dev/null +++ b/app/seasonal-planting-guide/styles.ts @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const PageContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const HeaderContainer = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const FilterContainer = styled.div` + display: flex; + flex-direction: row; + gap: 0.5rem; +`; + +export const StateOptionsContainer = styled.div` + display: flex; + flex-direction: row; + gap: 2rem; +`; diff --git a/components/FilterDropdownMultiple.tsx b/components/FilterDropdownMultiple.tsx new file mode 100644 index 0000000..b52b990 --- /dev/null +++ b/components/FilterDropdownMultiple.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { MultiSelect } from 'react-multi-select-component'; +import { DropdownOption } from '@/types/schema'; + +interface FilterDropdownProps { + value: DropdownOption[]; + setStateAction: React.Dispatch>; + options: DropdownOption[]; + placeholder: string; +} + +export default function FilterDropdownMultiple({ + value, + setStateAction, + options, + placeholder, +}: FilterDropdownProps) { + return ( + + ); +} diff --git a/components/FilterDropdownSingle.tsx b/components/FilterDropdownSingle.tsx new file mode 100644 index 0000000..bd2d470 --- /dev/null +++ b/components/FilterDropdownSingle.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import { DropdownOption } from '@/types/schema'; + +interface FilterDropdownProps { + name?: string; + id?: string; + value: string; + setStateAction: React.Dispatch>; + options: DropdownOption[]; + placeholder: string; +} + +export default function FilterDropdownSingle({ + name, + id, + value, + setStateAction, + options, + placeholder, +}: FilterDropdownProps) { + const [isOpen, setIsOpen] = useState(false); + + const handleChange = (event: React.ChangeEvent) => { + setStateAction(event.target.value); + setIsOpen(false); + }; + + const handleToggle = () => { + setIsOpen(!isOpen); + }; + + return ( + + ); +} diff --git a/components/PlantList.tsx b/components/PlantList.tsx index 04efe8e..82e2169 100644 --- a/components/PlantList.tsx +++ b/components/PlantList.tsx @@ -1,149 +1,68 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { getAllPlants } from '@/api/supabase/queries/plants'; -import { Plant } from '@/types/schema'; -import { processPlantMonth } from '@/utils/helpers'; +import { DropdownOption, Plant } from '@/types/schema'; +import { + checkGrowingSeason, + checkHarvestSeason, + checkPlantingType, + checkSearchTerm, +} from '@/utils/helpers'; interface PlantListProps { - harvestSeasonFilterValue: string; - plantingTypeFilterValue: string; - growingSeasonFilterValue: string; + harvestSeasonFilterValue: DropdownOption[]; + plantingTypeFilterValue: DropdownOption[]; + growingSeasonFilterValue: DropdownOption[]; + usStateFilterValue: string; + searchTerm: string; } export const PlantList = ({ harvestSeasonFilterValue, plantingTypeFilterValue, growingSeasonFilterValue, + usStateFilterValue, + searchTerm, }: PlantListProps) => { const [plants, setPlants] = useState([]); - const growingSeasonToIndex = new Map([ - ['Spring', [2, 3, 4]], - ['Summer', [5, 6, 7]], - ['Fall', [8, 9, 10]], - ['Winter', [11, 0, 1]], - ]); - - const monthToIndex = new Map([ - ['JANUARY', 0], - ['FEBRUARY', 1], - ['MARCH', 2], - ['APRIL', 3], - ['MAY', 4], - ['JUNE', 5], - ['JULY', 6], - ['AUGUST', 7], - ['SEPTEMBER', 8], - ['OCTOBER', 9], - ['NOVEMBER', 10], - ['DECEMBER', 11], - ]); useEffect(() => { const fetchPlantSeasonality = async () => { - // gets plants in Tennessee by default const plantList = await getAllPlants(); - const us_state = 'TENNESSEE'; + const us_state = usStateFilterValue; const filteredPlantList = plantList.filter( plant => plant.us_state === us_state, ); - setPlants(filteredPlantList); + const sortedAndFilteredPlantList = filteredPlantList.sort((a, b) => + a.plant_name.localeCompare(b.plant_name), + ); + setPlants(sortedAndFilteredPlantList); }; fetchPlantSeasonality(); - }, []); - - // Check if growingSeason matches the plant's growing season - const checkGrowingSeason = (plant: Plant) => { - // Automatically returns true if selected growing season is '' - if (!growingSeasonFilterValue) { - return true; - } - - // list of valid indexes for the growing season - // indexes are the months of the year - const validIndexes = growingSeasonToIndex.get(growingSeasonFilterValue); - - const isInRange = (start: number, end: number, validIndexes: number[]) => { - // Checks if the start and end months are within the valid range - if (start <= end) { - return validIndexes.some(index => index >= start && index <= end); - } else { - // Handle wrap-around case (e.g. NOVEMBER to FEBRUARY) - return validIndexes.some(index => index >= start || index <= end); - } - }; + }, [usStateFilterValue]); - // Handle late/early month logic - // Set late/early month to just the month using processPlantMonth - const indoorsStart = processPlantMonth(plant.indoors_start); - const indoorsEnd = processPlantMonth(plant.indoors_end); - const outdoorsStart = processPlantMonth(plant.outdoors_start); - const outdoorsEnd = processPlantMonth(plant.outdoors_end); - - // Checks if either indoor_start to indoor_end or outdoor_start to outdoor_end - // is within the valid range - // exclamation marks to assert values are not undefined - return ( - isInRange( - monthToIndex.get(indoorsStart)!, - monthToIndex.get(indoorsEnd)!, - validIndexes!, - ) || - isInRange( - monthToIndex.get(outdoorsStart)!, - monthToIndex.get(outdoorsEnd)!, - validIndexes!, - ) - ); - }; - - // Checks if harvestSeason matches the plant's harvest_season - const checkHarvestSeason = (plant: Plant) => { - // Automatically returns true if selected harvestSeason is '' - return ( - !harvestSeasonFilterValue || - plant.harvest_season === harvestSeasonFilterValue.toLocaleUpperCase() - ); - }; - - // Checks if plantingType matches the plant's planting type - const checkPlantingType = (plant: Plant) => { - // Automatically returns true if selected plantingType is '' - if (!plantingTypeFilterValue) { - return true; - } - - // Checking if corresponding start field in table is not null - // according to plantingType selected - if (plantingTypeFilterValue === 'Start Seeds Indoors') { - return plant.indoors_start !== null; - } else if (plantingTypeFilterValue === 'Start Seeds Outdoors') { - return plant.outdoors_start !== null; - } else if ( - plantingTypeFilterValue === 'Plant Seedlings/Transplant Outdoors' - ) { - return plant.transplant_start !== null; - } - }; - - const filterPlantList = (plant: Plant) => { - // Filters the plant list based on the selected filters - // Only returns true if plant passes all checks - return ( - checkGrowingSeason(plant) && - checkHarvestSeason(plant) && - checkPlantingType(plant) + const filteredPlantList = useMemo(() => { + return plants.filter( + plant => + checkGrowingSeason(growingSeasonFilterValue, plant) && + checkHarvestSeason(harvestSeasonFilterValue, plant) && + checkPlantingType(plantingTypeFilterValue, plant) && + checkSearchTerm(searchTerm, plant), ); - }; + }, [ + plants, + growingSeasonFilterValue, + harvestSeasonFilterValue, + plantingTypeFilterValue, + searchTerm, + ]); return (
- {plants - .filter(filterPlantList) - .sort((a, b) => a.plant_name.localeCompare(b.plant_name)) - .map((plant, key) => ( - //this should display PlantCalendarRows instead of this temporary div -
{plant.plant_name}
- ))} + {filteredPlantList.map((plant, key) => ( + //this should display PlantCalendarRows instead of this temporary div +
{plant.plant_name}
+ ))}
); }; diff --git a/components/SearchBar/index.tsx b/components/SearchBar/index.tsx new file mode 100644 index 0000000..46de72c --- /dev/null +++ b/components/SearchBar/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { SearchBarContainer, SearchBarInput } from './styles'; + +interface SearchBarProps { + searchTerm: string; + setSearchTerm: React.Dispatch>; +} + +export default function SearchBar({ + searchTerm, + setSearchTerm, +}: SearchBarProps) { + return ( + + setSearchTerm(e.target.value)} + /> + + ); +} diff --git a/components/SearchBar/styles.ts b/components/SearchBar/styles.ts new file mode 100644 index 0000000..ed96188 --- /dev/null +++ b/components/SearchBar/styles.ts @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const SearchBarContainer = styled.div` + background-color: #f7f7f7; + border: none; + border-radius: 8px; + width: 30%; +`; + +export const SearchBarInput = styled.input` + padding: 8px; + border: none; + background-color: #f7f7f7; + width: 100%; +`; diff --git a/package.json b/package.json index 5e33a10..068b170 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "next": "^14.2.10", "react": "^18", "react-dom": "^18", + "react-multi-select-component": "^4.3.4", "styled-components": "^6.1.13", "supabase": "^1.200.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7f1a55..9696c1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) + react-multi-select-component: + specifier: ^4.3.4 + version: 4.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) styled-components: specifier: ^6.1.13 version: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1467,6 +1470,12 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-multi-select-component@4.3.4: + resolution: {integrity: sha512-Ui/bzCbROF4WfKq3OKWyQJHmy/bd1mW7CQM+L83TfiltuVvHElhKEyPM3JzO9urIcWplBUKv+kyxqmEnd9jPcA==} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -3484,6 +3493,11 @@ snapshots: react-is@16.13.1: {} + react-multi-select-component@4.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 diff --git a/types/schema.d.ts b/types/schema.d.ts index 25f5e20..ec92e59 100644 --- a/types/schema.d.ts +++ b/types/schema.d.ts @@ -42,3 +42,8 @@ export interface UserPlants { date_harvested: string; planting_type: string; } + +export interface DropdownOption { + label: string; + value: string; +} diff --git a/utils/helpers.ts b/utils/helpers.ts index 5f34ec9..750fe95 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -1,4 +1,7 @@ -export function processPlantMonth(month: string) { +import { DropdownOption, Plant } from '@/types/schema'; + +// Helper function to process late/early month fields for checkGrowingSeason +function processPlantMonth(month: string) { // If field is not null and starts with 'LATE' or 'EARLY, // get substring after 'LATE_ or 'EARLY_' if (!month) { @@ -13,3 +16,138 @@ export function processPlantMonth(month: string) { return month; } } + +// Helper function to check if selected growing season(s) match plant's growing_season +export function checkGrowingSeason( + growingSeasonFilterValue: DropdownOption[], + plant: Plant, +) { + const growingSeasonToIndex = new Map([ + ['SPRING', [2, 3, 4]], + ['SUMMER', [5, 6, 7]], + ['FALL', [8, 9, 10]], + ['WINTER', [11, 0, 1]], + ]); + + const monthToIndex = new Map([ + ['JANUARY', 0], + ['FEBRUARY', 1], + ['MARCH', 2], + ['APRIL', 3], + ['MAY', 4], + ['JUNE', 5], + ['JULY', 6], + ['AUGUST', 7], + ['SEPTEMBER', 8], + ['OCTOBER', 9], + ['NOVEMBER', 10], + ['DECEMBER', 11], + ]); + + // Automatically returns true if selected growing season is [] + if (growingSeasonFilterValue.length === 0) { + return true; + } + + // For each growingSeason selected, collect the valid indexes (months) + let validIndexes: number[] = []; + for (const growingSeason of growingSeasonFilterValue) { + validIndexes = validIndexes.concat( + growingSeasonToIndex.get(growingSeason.value)!, + ); + } + + const isInRange = (start: number, end: number, validIndexes: number[]) => { + // Checks if the start and end months are within the valid range + if (start <= end) { + return validIndexes.some(index => index >= start && index <= end); + } else { + // Handle wrap-around case (e.g. NOVEMBER to FEBRUARY) + return validIndexes.some(index => index >= start || index <= end); + } + }; + + // Handle late/early month logic + // Set late/early month to just the month using processPlantMonth + const indoorsStart = processPlantMonth(plant.indoors_start); + const indoorsEnd = processPlantMonth(plant.indoors_end); + const outdoorsStart = processPlantMonth(plant.outdoors_start); + const outdoorsEnd = processPlantMonth(plant.outdoors_end); + + // Checks if either indoor_start to indoor_end or outdoor_start to outdoor_end + // is within the valid range of months + // exclamation marks to assert values are not undefined + return ( + isInRange( + monthToIndex.get(indoorsStart)!, + monthToIndex.get(indoorsEnd)!, + validIndexes!, + ) || + isInRange( + monthToIndex.get(outdoorsStart)!, + monthToIndex.get(outdoorsEnd)!, + validIndexes!, + ) + ); +} + +// Helper function to check if selected harvest season(s) match plant's harvest_season +export function checkHarvestSeason( + harvestSeasonFilterValue: DropdownOption[], + plant: Plant, +) { + // Automatically returns true if selected harvestSeason is [] + if (harvestSeasonFilterValue.length === 0) { + return true; + } + + // For each harvestSeason selected, check if plant's harvest_season matches harvestSeason + // If it does, add true to harvestSeasonBoolean + const harvestSeasonBoolean: boolean[] = []; + for (const harvestSeason of harvestSeasonFilterValue) { + harvestSeasonBoolean.push(plant.harvest_season === harvestSeason.value); + } + + // Return true if any of the harvestSeasonBooleans are true + return harvestSeasonBoolean.includes(true); +} + +// Helper function to check if selected planting type(s) match plant's planting_type +export function checkPlantingType( + plantingTypeFilterValue: DropdownOption[], + plant: Plant, +) { + // Automatically returns true if selected plantingType is [] + if (plantingTypeFilterValue.length === 0) { + return true; + } + + // For each plantingType selected, check if corresponding start field in table is not null + // If it is not null, add true to plantingTypeBoolean + const plantingTypeBoolean: boolean[] = []; + for (const plantingType of plantingTypeFilterValue) { + if (plantingType.value === 'Start Seeds Indoors') { + plantingTypeBoolean.push(plant.indoors_start !== null); + } else if (plantingType.value === 'Start Seeds Outdoors') { + plantingTypeBoolean.push(plant.outdoors_start !== null); + } else if (plantingType.value === 'Plant Seedlings/Transplant Outdoors') { + plantingTypeBoolean.push(plant.transplant_start !== null); + } + } + + // Return true if any of the plantingTypeBooleans are true + return plantingTypeBoolean.includes(true); +} + +export function checkSearchTerm(searchTerm: string, plant: Plant) { + // Automatically returns true if searchTerm is '' + if (searchTerm === '') { + return true; + } + + // Process searchTerm to remove leading and trailing spaces + searchTerm = searchTerm.trim(); + + // Check if plant_name contains searchTerm + return plant.plant_name.toLowerCase().includes(searchTerm.toLowerCase()); +}