From 34ceba2fa39d1cd12ebf3c91a0d129084b2470af Mon Sep 17 00:00:00 2001 From: Catherine Tan Date: Mon, 4 Nov 2024 14:06:31 -0800 Subject: [PATCH 01/16] [chore] fix Plant fields in add-details --- app/add-details/page.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/add-details/page.tsx b/app/add-details/page.tsx index a420705..f91fc98 100644 --- a/app/add-details/page.tsx +++ b/app/add-details/page.tsx @@ -20,12 +20,12 @@ export default function Home() { harvest_season: 'SPRING', water_frequency: 'string', weeding_frequency: 'string', - plant_seed_indoors_start: 'string', - plant_seed_indoors_end: 'string', - plant_seed_outdoors_start: 'string', - plant_seed_outdoors_end: 'string', - plant_transplant_start: 'string', - plant_transplant_end: 'string', + indoors_start: 'string', + indoors_end: 'string', + outdoors_start: 'string', + outdoors_end: 'string', + transplant_start: 'string', + transplant_end: 'string', harvest_start: 'string', harvest_end: 'string', beginner_friendly: true, @@ -42,12 +42,12 @@ export default function Home() { harvest_season: 'SPRING', water_frequency: 'string', weeding_frequency: 'string', - plant_seed_indoors_start: 'string', - plant_seed_indoors_end: 'string', - plant_seed_outdoors_start: 'string', - plant_seed_outdoors_end: 'string', - plant_transplant_start: 'string', - plant_transplant_end: 'string', + indoors_start: 'string', + indoors_end: 'string', + outdoors_start: 'string', + outdoors_end: 'string', + transplant_start: 'string', + transplant_end: 'string', harvest_start: 'string', harvest_end: 'string', beginner_friendly: true, From 40894a76ab89a17c8090a0ae6899288794d3b6d0 Mon Sep 17 00:00:00 2001 From: Kyle Ramachandran <156966341+kylezryr@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:27:25 -0800 Subject: [PATCH 02/16] [feat] Implement state filter and multi select filters (#27) Co-authored-by: Catherine Tan --- app/seasonal-planting-guide/page.tsx | 160 ++++++++++++++++++-------- app/seasonal-planting-guide/styles.ts | 25 ++++ components/FilterDropdownMultiple.tsx | 29 +++++ components/FilterDropdownSingle.tsx | 52 +++++++++ components/PlantList.tsx | 159 +++++++------------------ components/SearchBar/index.tsx | 23 ++++ components/SearchBar/styles.ts | 15 +++ package.json | 1 + pnpm-lock.yaml | 14 +++ types/schema.d.ts | 5 + utils/helpers.ts | 140 +++++++++++++++++++++- 11 files changed, 451 insertions(+), 172 deletions(-) create mode 100644 app/seasonal-planting-guide/styles.ts create mode 100644 components/FilterDropdownMultiple.tsx create mode 100644 components/FilterDropdownSingle.tsx create mode 100644 components/SearchBar/index.tsx create mode 100644 components/SearchBar/styles.ts 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()); +} From 32b4023906275aa740e8caf60aa1f9781230e68a Mon Sep 17 00:00:00 2001 From: Sashank Balusu <62404069+SashankBalusu@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:38:54 -0800 Subject: [PATCH 03/16] [feat] refactor add plant details (#25) Co-authored-by: Catherine Tan --- app/add-details/page.tsx | 192 +++++++++++++++--------------- components/PlantDetails/index.tsx | 34 ++++-- 2 files changed, 119 insertions(+), 107 deletions(-) diff --git a/app/add-details/page.tsx b/app/add-details/page.tsx index f91fc98..81b45b3 100644 --- a/app/add-details/page.tsx +++ b/app/add-details/page.tsx @@ -7,126 +7,130 @@ import { updateUserPlants } from '@/api/supabase/queries/updateUserPlants'; import PlantDetails from '@/components/PlantDetails'; import { Plant, UserPlants } from '@/types/schema'; +const plants: Plant[] = [ + { + id: 'cfed129c-1cdf-4089-89d2-83ae2fb2f83d', + plant_name: 'cabbage', + us_state: 'string', + harvest_season: 'SPRING', + water_frequency: 'string', + weeding_frequency: 'string', + indoors_start: 'string', + indoors_end: 'string', + outdoors_start: 'string', + outdoors_end: 'string', + transplant_start: 'string', + transplant_end: 'string', + harvest_start: 'string', + harvest_end: 'string', + beginner_friendly: true, + plant_tips: 'string', + img: 'string', + difficulty_level: 'HARD', + sunlight_min_hours: 1, + sunlight_max_hours: 1, + }, + { + id: '8f25fca8-6e86-486b-9a2b-79f68efa3658', + plant_name: 'tomato', + us_state: 'string', + harvest_season: 'SPRING', + water_frequency: 'string', + weeding_frequency: 'string', + indoors_start: 'string', + indoors_end: 'string', + outdoors_start: 'string', + outdoors_end: 'string', + transplant_start: 'string', + transplant_end: 'string', + harvest_start: 'string', + harvest_end: 'string', + beginner_friendly: true, + plant_tips: 'string', + img: 'string', + difficulty_level: 'HARD', + sunlight_min_hours: 1, + sunlight_max_hours: 1, + }, +]; +const user_id: UUID = '0802d796-ace8-480d-851b-d16293c74a21'; + export default function Home() { const [currentIndex, setCurrentIndex] = useState(1); - const [details, setDetails] = useState[]>([]); + const [details, setDetails] = useState[]>( + plants.map(plant => ({ plant_id: plant.id, user_id: user_id })), + ); const router = useRouter(); - const plants: Plant[] = [ - { - id: '43c19f80-8205-4d03-b323-05c220550bf0', - plant_name: 'cabbbage', - us_state: 'string', - harvest_season: 'SPRING', - water_frequency: 'string', - weeding_frequency: 'string', - indoors_start: 'string', - indoors_end: 'string', - outdoors_start: 'string', - outdoors_end: 'string', - transplant_start: 'string', - transplant_end: 'string', - harvest_start: 'string', - harvest_end: 'string', - beginner_friendly: true, - plant_tips: 'string', - img: 'string', - difficulty_level: 'HARD', - sunlight_min_hours: 1, - sunlight_max_hours: 1, - }, - { - id: '43c19f80-8205-4d03-b323-05c220550bf0', - plant_name: 'tomatoooooo', - us_state: 'string', - harvest_season: 'SPRING', - water_frequency: 'string', - weeding_frequency: 'string', - indoors_start: 'string', - indoors_end: 'string', - outdoors_start: 'string', - outdoors_end: 'string', - transplant_start: 'string', - transplant_end: 'string', - harvest_start: 'string', - harvest_end: 'string', - beginner_friendly: true, - plant_tips: 'string', - img: 'string', - difficulty_level: 'HARD', - sunlight_min_hours: 1, - sunlight_max_hours: 1, - }, - ]; - const user_id: UUID = 'e72af66d-7aae-45f6-935a-187197749d9f'; + const getDefaultDate = () => new Date().toISOString().substring(0, 10); + // Navigate between plants and save input data function move(steps: number) { - // if ur not at the end of the plant details flow update details to store what was in the inputs - if (currentIndex != plants.length + 1) { - const updatedDetails = [...details]; - const plantID = plants[currentIndex - 1]['id']; - const date = (document.getElementById('date')! as HTMLInputElement).value; - const plant_type = ( - document.getElementById('plantingType')! as HTMLInputElement - ).value; - updatedDetails[currentIndex - 1] = { - date_added: date, - planting_type: plant_type, - plant_id: plantID, - }; - setDetails(updatedDetails); + const currentDetail = details[currentIndex - 1]; + + // Set curr date in details to default date if not on submission page + if ( + (!currentDetail || !currentDetail.date_added) && + currentIndex <= plants.length + ) { + updateInput('date_added', getDefaultDate()); } - //if param steps is less than 0 and ur not at start, move back - if (steps < 0 && currentIndex != 1) { - setCurrentIndex(currentIndex => currentIndex - 1); + // For valid moves, move to next page + if ( + steps !== 0 && + currentIndex + steps > 0 && + currentIndex + steps <= plants.length + 1 + ) { + setCurrentIndex(prevIndex => prevIndex + steps); + } + } - //retrieve input for that element - //updateInput() - //if param steps is more than 0 and ur not at the end, move forward - } else if (steps > 0 && currentIndex != plants.length + 1) { - setCurrentIndex(currentIndex => currentIndex + 1); + function disableNext() { + // disable next if planting type is "SELECT" or undefined + return !( + details[currentIndex - 1].planting_type && + details[currentIndex - 1].planting_type !== 'SELECT' + ); + } - //retrieve input for that element - //updateInput() - } + function updateInput(field: string, value: string) { + const updatedDetails = [...details]; + updatedDetails[currentIndex - 1] = { + ...updatedDetails[currentIndex - 1], + [field]: value, + }; + setDetails(updatedDetails); } - function updateDB(user_id: UUID) { - //console.log(details) - updateUserPlants(user_id, details); + + async function updateDB() { + await updateUserPlants(user_id, details); router.push('/view-plants'); } - function getDetails() { - if (details[currentIndex - 1]) { - return details[currentIndex - 1]; - } - return undefined; - } return (
- {currentIndex != plants.length + 1 && ( + {currentIndex !== plants.length + 1 && (
+ date={details[currentIndex - 1].date_added || getDefaultDate()} + plantingType={details[currentIndex - 1].planting_type || 'SELECT'} + onDateChange={date => updateInput('date_added', date)} + onPlantingTypeChange={type => updateInput('planting_type', type)} + />

{currentIndex} / {plants.length}

- +
)} - {currentIndex == plants.length + 1 && ( + {currentIndex === plants.length + 1 && (
- +
)}
diff --git a/components/PlantDetails/index.tsx b/components/PlantDetails/index.tsx index dc21f69..543e37b 100644 --- a/components/PlantDetails/index.tsx +++ b/components/PlantDetails/index.tsx @@ -1,29 +1,37 @@ -import { Plant, UserPlants } from '@/types/schema'; +import { Plant } from '@/types/schema'; export default function PlantDetails({ - detail, plant, + date, + plantingType, + onDateChange, + onPlantingTypeChange, }: { - detail: Partial; plant: Plant; + date: string; + plantingType: string; + onDateChange: (date: string) => void; + onPlantingTypeChange: (type: string) => void; }) { - function getDate() { - if (detail) { - return detail['date_added']; - } - const curr = new Date(); - curr.setDate(curr.getDate()); - return curr.toISOString().substring(0, 10); - } return (

{plant.plant_name}

- + onDateChange(e.target.value)} + /> - onPlantingTypeChange(e.target.value)} + > + From 1f9cc704c2fa38023ee2364d00b357db7b4a28a6 Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 6 Oct 2024 17:08:19 -0700 Subject: [PATCH 04/16] plot status and state pages --- app/onboarding/plotstatus/page.tsx | 31 +++++++++++++++++++++++++++++ app/onboarding/state/page.tsx | 32 ++++++++++++++++++++++++++++++ hooks/useProfile.ts | 31 +++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 app/onboarding/plotstatus/page.tsx create mode 100644 app/onboarding/state/page.tsx create mode 100644 hooks/useProfile.ts diff --git a/app/onboarding/plotstatus/page.tsx b/app/onboarding/plotstatus/page.tsx new file mode 100644 index 0000000..5cd9a20 --- /dev/null +++ b/app/onboarding/plotstatus/page.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useProfile } from '@/hooks/useProfile'; +import { + Button, + ButtonContainer, + ContentContainer, + PageContainer, +} from '../styles'; + +export default function Page() { + const { addProfile } = useProfile(); + + const handleButtonClick = (state: string) => { + const newProfile = { state, email: '', phone_num: '', user_type: '' }; + addProfile(newProfile); + console.log('test'); + }; + return ( + + +

Plot Status

+

Do you own a plot to plant in?

+ + + + +
+
+ ); +} diff --git a/app/onboarding/state/page.tsx b/app/onboarding/state/page.tsx new file mode 100644 index 0000000..ae2bb07 --- /dev/null +++ b/app/onboarding/state/page.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useProfile } from '@/hooks/useProfile'; +import { + Button, + ButtonContainer, + ContentContainer, + PageContainer, +} from '../styles'; + +export default function Page() { + const { addProfile } = useProfile(); + + const handleButtonClick = (state: string) => { + const newProfile = { state, email: '', phone_num: '', user_type: '' }; + addProfile(newProfile); + console.log('test'); + }; + return ( + + +

Choose Your state

+ + + + +
+
+ ); +} diff --git a/hooks/useProfile.ts b/hooks/useProfile.ts new file mode 100644 index 0000000..3543922 --- /dev/null +++ b/hooks/useProfile.ts @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import { Profile } from '../types/schema'; + +const initialProfiles: Profile[] = []; + +export const useProfile = () => { + const [profiles, setProfiles] = useState(initialProfiles); + + const addProfile = (newProfile: Profile) => { + setProfiles(prev => [...prev, newProfile]); + }; + + const updateProfile = (index: number, updates: Partial) => { + setProfiles(prev => + prev.map((profile, i) => + i === index ? { ...profile, ...updates } : profile, + ), + ); + }; + + const removeProfile = (index: number) => { + setProfiles(prev => prev.filter((_, i) => i !== index)); + }; + + return { + profiles, + addProfile, + updateProfile, + removeProfile, + }; +}; From 96e315b760e5ad08119d234f5aee08b48092c8fa Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 13 Oct 2024 15:47:03 -0700 Subject: [PATCH 05/16] removed profile functions from plot status and state page for lint --- app/onboarding/plotstatus/page.tsx | 2 -- app/onboarding/state/page.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/onboarding/plotstatus/page.tsx b/app/onboarding/plotstatus/page.tsx index 5cd9a20..bf01d8e 100644 --- a/app/onboarding/plotstatus/page.tsx +++ b/app/onboarding/plotstatus/page.tsx @@ -12,8 +12,6 @@ export default function Page() { const { addProfile } = useProfile(); const handleButtonClick = (state: string) => { - const newProfile = { state, email: '', phone_num: '', user_type: '' }; - addProfile(newProfile); console.log('test'); }; return ( diff --git a/app/onboarding/state/page.tsx b/app/onboarding/state/page.tsx index ae2bb07..2ad74f0 100644 --- a/app/onboarding/state/page.tsx +++ b/app/onboarding/state/page.tsx @@ -12,8 +12,6 @@ export default function Page() { const { addProfile } = useProfile(); const handleButtonClick = (state: string) => { - const newProfile = { state, email: '', phone_num: '', user_type: '' }; - addProfile(newProfile); console.log('test'); }; return ( From e0cc58da6ca877d2b673e17aec0153c039f9dacd Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 13 Oct 2024 15:53:28 -0700 Subject: [PATCH 06/16] removed folders for individual pages, primarily for lint --- app/onboarding/plotstatus/page.tsx | 29 ----------------------------- app/onboarding/state/page.tsx | 30 ------------------------------ 2 files changed, 59 deletions(-) delete mode 100644 app/onboarding/plotstatus/page.tsx delete mode 100644 app/onboarding/state/page.tsx diff --git a/app/onboarding/plotstatus/page.tsx b/app/onboarding/plotstatus/page.tsx deleted file mode 100644 index bf01d8e..0000000 --- a/app/onboarding/plotstatus/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { useProfile } from '@/hooks/useProfile'; -import { - Button, - ButtonContainer, - ContentContainer, - PageContainer, -} from '../styles'; - -export default function Page() { - const { addProfile } = useProfile(); - - const handleButtonClick = (state: string) => { - console.log('test'); - }; - return ( - - -

Plot Status

-

Do you own a plot to plant in?

- - - - -
-
- ); -} diff --git a/app/onboarding/state/page.tsx b/app/onboarding/state/page.tsx deleted file mode 100644 index 2ad74f0..0000000 --- a/app/onboarding/state/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { useProfile } from '@/hooks/useProfile'; -import { - Button, - ButtonContainer, - ContentContainer, - PageContainer, -} from '../styles'; - -export default function Page() { - const { addProfile } = useProfile(); - - const handleButtonClick = (state: string) => { - console.log('test'); - }; - return ( - - -

Choose Your state

- - - - -
-
- ); -} From 41bb89b263ebe0ce2d669b9b947ea5e576e9bafe Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sat, 26 Oct 2024 14:42:30 -0700 Subject: [PATCH 07/16] [feat] ProfileProvider implementation, lint + prettier fixes --- app/layout.tsx | 5 +- app/onboarding/page.tsx | 86 +++++++++++++++++----------------- app/page.tsx | 10 ++-- utils/ProfileProvider.tsx | 99 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 49 deletions(-) create mode 100644 utils/ProfileProvider.tsx diff --git a/app/layout.tsx b/app/layout.tsx index c0101bc..5e9ea9c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import StyledComponentsRegistry from '@/lib/registry'; +import { ProfileProvider } from '@/utils/ProfileProvider'; // font definitions const sans = Inter({ @@ -22,7 +23,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx index c75cf7d..4c418dc 100644 --- a/app/onboarding/page.tsx +++ b/app/onboarding/page.tsx @@ -1,8 +1,7 @@ 'use client'; import React, { useState } from 'react'; -import { upsertProfile } from '@/api/supabase/queries/profiles'; -import { Profile } from '@/types/schema'; +import { ProfileProvider, useProfile } from '@/utils/ProfileProvider'; // Define the possible options for each question const states = ['Tennessee', 'Missouri']; @@ -115,58 +114,57 @@ const OnboardingFlow = () => { const handleBack = () => { setStep(step - 1); }; - + const { setProfile } = useProfile(); const handleSubmit = async () => { - const profile: Profile = { - user_id: '2abd7296-374a-42d1-bb4f-b813da1615ae', - state: selectedState, - user_type: selectedGardenType, - has_plot: selectedPlot, - }; try { - upsertProfile(profile); + await setProfile({ + state: selectedState, + user_type: selectedGardenType, + has_plot: selectedPlot, + }); + console.log('Profile updated successfully'); } catch (error) { - console.error('Error upserting profile:', error); - throw new Error('Error upserting profile'); - } finally { - //TODO: Remove console log. - console.log('Submitted data: ', profile); + console.error('Error submitting profile:', error); } - // Handle form submission, e.g., send to a server or display a confirmation }; + // Handle form submission, e.g., send to a server or display a confirmation return ( -
- {step === 1 && ( - - )} - {step === 2 && ( - - )} - {step === 3 && ( - - )} - +
- {step > 1 && } - {step < 3 && ( - + {step === 1 && ( + + )} + {step === 2 && ( + )} - {step === 3 && } + {step === 3 && ( + + )} + +
+ {step > 1 && } + {step < 3 && ( + + )} + {step === 3 && } +
-
+ ); }; - export default OnboardingFlow; diff --git a/app/page.tsx b/app/page.tsx index d88a5b5..2795f62 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,11 +1,13 @@ -import BPLogo from '@/assets/images/bp-logo.png'; -import { Container, Image } from './page.style'; +import { ProfileProvider } from '@/utils/ProfileProvider'; +import OnboardingFlow from './onboarding/page'; +import { Container } from './page.style'; export default function Home() { return ( - Blueprint Logo -

Open up app/page.tsx to get started!

+ + +
); } diff --git a/utils/ProfileProvider.tsx b/utils/ProfileProvider.tsx new file mode 100644 index 0000000..65dc1cf --- /dev/null +++ b/utils/ProfileProvider.tsx @@ -0,0 +1,99 @@ +'use client'; + +import React, { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { upsertProfile } from '@/api/supabase/queries/profiles'; +import { Profile } from '@/types/schema'; + +// Define a placeholder user ID for development purposes +const placeholderUserId = '2abd7296-374a-42d1-bb4f-b813da1615ae'; + +interface ProfileContextType { + profileData: Profile | null; + profileReady: boolean; + setProfile: (newProfileData: Partial) => Promise; + loadProfile: () => Promise; +} + +const ProfileContext = createContext(undefined); + +export const useProfile = () => { + const context = useContext(ProfileContext); + if (!context) { + throw new Error('useProfile must be used within a ProfileProvider'); + } + return context; +}; + +export const ProfileProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [profileData, setProfileData] = useState(null); + const [profileReady, setProfileReady] = useState(false); + + const loadProfile = useCallback(async () => { + setProfileReady(false); + + try { + const profile: Profile = { + user_id: placeholderUserId, + state: '', + user_type: '', + has_plot: false, + }; + // Fetch or upsert the profile for the placeholder user ID + const fetchedProfile = await upsertProfile(profile); + setProfileData(fetchedProfile); + } catch (error) { + console.error('Error loading profile:', error); + } finally { + setProfileReady(true); + } + }, []); + + const setProfile = useCallback( + async (newProfileData: Partial) => { + const profileToUpdate: Profile = { + ...profileData!, + ...newProfileData, + user_id: placeholderUserId, // Using placeholder user ID for now + }; + + try { + const updatedProfile = await upsertProfile(profileToUpdate); + setProfileData(updatedProfile); + } catch (error) { + console.error('Error updating profile:', error); + throw new Error('Error updating profile'); + } + }, + [profileData], + ); + + useEffect(() => { + loadProfile(); + }, [loadProfile]); + + const providerValue = useMemo( + () => ({ + profileData, + profileReady, + setProfile, + loadProfile, + }), + [profileData, profileReady, setProfile, loadProfile], + ); + + return ( + + {children} + + ); +}; From 57fb90b4b9cbfe985a828fdfea961de731fcadad Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 6 Oct 2024 17:08:19 -0700 Subject: [PATCH 08/16] plot status and state pages --- app/onboarding/plotstatus/page.tsx | 31 +++++++++++++++++++++++++++++ app/onboarding/state/page.tsx | 32 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 app/onboarding/plotstatus/page.tsx create mode 100644 app/onboarding/state/page.tsx diff --git a/app/onboarding/plotstatus/page.tsx b/app/onboarding/plotstatus/page.tsx new file mode 100644 index 0000000..5cd9a20 --- /dev/null +++ b/app/onboarding/plotstatus/page.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useProfile } from '@/hooks/useProfile'; +import { + Button, + ButtonContainer, + ContentContainer, + PageContainer, +} from '../styles'; + +export default function Page() { + const { addProfile } = useProfile(); + + const handleButtonClick = (state: string) => { + const newProfile = { state, email: '', phone_num: '', user_type: '' }; + addProfile(newProfile); + console.log('test'); + }; + return ( + + +

Plot Status

+

Do you own a plot to plant in?

+ + + + +
+
+ ); +} diff --git a/app/onboarding/state/page.tsx b/app/onboarding/state/page.tsx new file mode 100644 index 0000000..ae2bb07 --- /dev/null +++ b/app/onboarding/state/page.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useProfile } from '@/hooks/useProfile'; +import { + Button, + ButtonContainer, + ContentContainer, + PageContainer, +} from '../styles'; + +export default function Page() { + const { addProfile } = useProfile(); + + const handleButtonClick = (state: string) => { + const newProfile = { state, email: '', phone_num: '', user_type: '' }; + addProfile(newProfile); + console.log('test'); + }; + return ( + + +

Choose Your state

+ + + + +
+
+ ); +} From 5caadc49fc521606d2bc38ab0efcbba84ec39fad Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 13 Oct 2024 15:47:03 -0700 Subject: [PATCH 09/16] removed profile functions from plot status and state page for lint --- app/onboarding/plotstatus/page.tsx | 2 -- app/onboarding/state/page.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/onboarding/plotstatus/page.tsx b/app/onboarding/plotstatus/page.tsx index 5cd9a20..bf01d8e 100644 --- a/app/onboarding/plotstatus/page.tsx +++ b/app/onboarding/plotstatus/page.tsx @@ -12,8 +12,6 @@ export default function Page() { const { addProfile } = useProfile(); const handleButtonClick = (state: string) => { - const newProfile = { state, email: '', phone_num: '', user_type: '' }; - addProfile(newProfile); console.log('test'); }; return ( diff --git a/app/onboarding/state/page.tsx b/app/onboarding/state/page.tsx index ae2bb07..2ad74f0 100644 --- a/app/onboarding/state/page.tsx +++ b/app/onboarding/state/page.tsx @@ -12,8 +12,6 @@ export default function Page() { const { addProfile } = useProfile(); const handleButtonClick = (state: string) => { - const newProfile = { state, email: '', phone_num: '', user_type: '' }; - addProfile(newProfile); console.log('test'); }; return ( From d461ca49dc2b40c0ba17c4697a24b54b18b35bdd Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 13 Oct 2024 15:53:28 -0700 Subject: [PATCH 10/16] removed folders for individual pages, primarily for lint --- app/onboarding/plotstatus/page.tsx | 29 ----------------------------- app/onboarding/state/page.tsx | 30 ------------------------------ 2 files changed, 59 deletions(-) delete mode 100644 app/onboarding/plotstatus/page.tsx delete mode 100644 app/onboarding/state/page.tsx diff --git a/app/onboarding/plotstatus/page.tsx b/app/onboarding/plotstatus/page.tsx deleted file mode 100644 index bf01d8e..0000000 --- a/app/onboarding/plotstatus/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { useProfile } from '@/hooks/useProfile'; -import { - Button, - ButtonContainer, - ContentContainer, - PageContainer, -} from '../styles'; - -export default function Page() { - const { addProfile } = useProfile(); - - const handleButtonClick = (state: string) => { - console.log('test'); - }; - return ( - - -

Plot Status

-

Do you own a plot to plant in?

- - - - -
-
- ); -} diff --git a/app/onboarding/state/page.tsx b/app/onboarding/state/page.tsx deleted file mode 100644 index 2ad74f0..0000000 --- a/app/onboarding/state/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { useProfile } from '@/hooks/useProfile'; -import { - Button, - ButtonContainer, - ContentContainer, - PageContainer, -} from '../styles'; - -export default function Page() { - const { addProfile } = useProfile(); - - const handleButtonClick = (state: string) => { - console.log('test'); - }; - return ( - - -

Choose Your state

- - - - -
-
- ); -} From ceed721f8d739debb3649367ba7e8206546e3e27 Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 3 Nov 2024 15:43:59 -0800 Subject: [PATCH 11/16] [refactor], updated profile UserTypeEnum, separate logic to handle has plot, replaced react.fc with regular function definitions --- app/layout.tsx | 2 +- app/onboarding/page.tsx | 44 ++++++++++++++--------- app/page.tsx | 5 +-- types/schema.d.ts | 5 ++- utils/ProfileProvider.tsx | 73 ++++++++++++++++++++++----------------- 5 files changed, 72 insertions(+), 57 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 5e9ea9c..52056e7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import StyledComponentsRegistry from '@/lib/registry'; -import { ProfileProvider } from '@/utils/ProfileProvider'; +import ProfileProvider from '@/utils/ProfileProvider'; // font definitions const sans = Inter({ diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx index 4c418dc..88711bb 100644 --- a/app/onboarding/page.tsx +++ b/app/onboarding/page.tsx @@ -1,15 +1,19 @@ 'use client'; import React, { useState } from 'react'; -import { ProfileProvider, useProfile } from '@/utils/ProfileProvider'; +import { UUID } from 'crypto'; +import { Profile, UserTypeEnum } from '@/types/schema'; +import ProfileProvider, { useProfile } from '@/utils/ProfileProvider'; // Define the possible options for each question const states = ['Tennessee', 'Missouri']; -const gardenTypes = ['Individual', 'Community', 'School']; +const gardenTypes: UserTypeEnum[] = ['INDIV', 'SCHOOL', 'ORG']; const plotOptions = [ { label: 'yes', value: true }, { label: 'no', value: false }, ]; +const placeholderUserId: UUID = '2abd7296-374a-42d1-bb4f-b813da1615ae'; + //Interfaces and props to avoid typ errors on Lint interface StateSelectionProps { selectedState: string; @@ -17,8 +21,8 @@ interface StateSelectionProps { } interface GardenTypeSelectionProps { - selectedGardenType: string; - setSelectedGardenType: React.Dispatch>; + selectedGardenType: UserTypeEnum; + setSelectedGardenType: React.Dispatch>; } interface PlotSelectionProps { @@ -27,10 +31,10 @@ interface PlotSelectionProps { } // Select State -const StateSelection: React.FC = ({ +const StateSelection = ({ selectedState, setSelectedState, -}) => { +}: StateSelectionProps) => { return (

Which state do you live in?

@@ -53,10 +57,10 @@ const StateSelection: React.FC = ({ // Step 2: Select garden type -const GardenTypeSelection: React.FC = ({ +const GardenTypeSelection = ({ selectedGardenType, setSelectedGardenType, -}) => { +}: GardenTypeSelectionProps) => { return (

What type of garden do you want to create?

@@ -67,7 +71,9 @@ const GardenTypeSelection: React.FC = ({ name="gardenType" value={type} checked={selectedGardenType === type} - onChange={e => setSelectedGardenType(e.target.value)} + onChange={e => + setSelectedGardenType(e.target.value as UserTypeEnum) + } /> {type} @@ -77,10 +83,10 @@ const GardenTypeSelection: React.FC = ({ }; // Step 3: Do you have a plot? -const PlotSelection: React.FC = ({ +const PlotSelection = ({ selectedPlot, setSelectedPlot, -}) => { +}: PlotSelectionProps) => { return (

Do you already have a plot?

@@ -102,9 +108,11 @@ const PlotSelection: React.FC = ({ // Main Onboarding Component const OnboardingFlow = () => { + const { setProfile, updateHasPlot } = useProfile(); const [step, setStep] = useState(1); const [selectedState, setSelectedState] = useState(''); - const [selectedGardenType, setSelectedGardenType] = useState(''); + const [selectedGardenType, setSelectedGardenType] = + useState('INDIV'); const [selectedPlot, setSelectedPlot] = useState(false); const handleNext = () => { @@ -114,15 +122,17 @@ const OnboardingFlow = () => { const handleBack = () => { setStep(step - 1); }; - const { setProfile } = useProfile(); const handleSubmit = async () => { try { - await setProfile({ + const profileToUpload: Profile = { + user_id: placeholderUserId, state: selectedState, user_type: selectedGardenType, - has_plot: selectedPlot, - }); - console.log('Profile updated successfully'); + }; + + await setProfile(profileToUpload); + await updateHasPlot(selectedPlot); // Update has_plot + console.log('Profile and has_plot updated successfully'); } catch (error) { console.error('Error submitting profile:', error); } diff --git a/app/page.tsx b/app/page.tsx index 2795f62..1e948e7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,10 @@ -import { ProfileProvider } from '@/utils/ProfileProvider'; import OnboardingFlow from './onboarding/page'; import { Container } from './page.style'; export default function Home() { return ( - - - + ); } diff --git a/types/schema.d.ts b/types/schema.d.ts index ec92e59..24ba33c 100644 --- a/types/schema.d.ts +++ b/types/schema.d.ts @@ -1,14 +1,13 @@ import type { UUID } from 'crypto'; export type SeasonEnum = 'SPRING' | 'SUMMER' | 'FALL' | 'WINTER'; - +export type UserTypeEnum = 'INDIV' | 'SCHOOL' | 'ORG'; export type DifficultyLevelEnum = 'EASY' | 'MODERATE' | 'HARD'; export interface Profile { user_id: UUID; state: string; - user_type: string; - has_plot: boolean; + user_type: UserTypeEnum; } export interface Plant { diff --git a/utils/ProfileProvider.tsx b/utils/ProfileProvider.tsx index 65dc1cf..d51f5fc 100644 --- a/utils/ProfileProvider.tsx +++ b/utils/ProfileProvider.tsx @@ -15,15 +15,16 @@ import { Profile } from '@/types/schema'; // Define a placeholder user ID for development purposes const placeholderUserId = '2abd7296-374a-42d1-bb4f-b813da1615ae'; -interface ProfileContextType { +export interface ProfileContextType { profileData: Profile | null; profileReady: boolean; - setProfile: (newProfileData: Partial) => Promise; + has_plot: boolean; + setProfile: (completeProfile: Profile) => Promise; // Now expects full Profile loadProfile: () => Promise; + updateHasPlot: (plotValue: boolean) => Promise; // Add this line } const ProfileContext = createContext(undefined); - export const useProfile = () => { const context = useContext(ProfileContext); if (!context) { @@ -32,25 +33,28 @@ export const useProfile = () => { return context; }; -export const ProfileProvider: React.FC<{ children: ReactNode }> = ({ +interface ProfileProviderProps { + children: ReactNode; +} +export default function ProfileProvider({ children, -}) => { +}: ProfileProviderProps): JSX.Element { const [profileData, setProfileData] = useState(null); const [profileReady, setProfileReady] = useState(false); + const [hasPlot, setHasPlot] = useState(false); const loadProfile = useCallback(async () => { setProfileReady(false); - try { const profile: Profile = { user_id: placeholderUserId, state: '', - user_type: '', - has_plot: false, + user_type: 'INDIV', //default for now + // Removed has_plot as it's not part of Profile }; - // Fetch or upsert the profile for the placeholder user ID const fetchedProfile = await upsertProfile(profile); setProfileData(fetchedProfile); + // Set has_plot independently, assuming it needs separate handling } catch (error) { console.error('Error loading profile:', error); } finally { @@ -58,37 +62,42 @@ export const ProfileProvider: React.FC<{ children: ReactNode }> = ({ } }, []); - const setProfile = useCallback( - async (newProfileData: Partial) => { - const profileToUpdate: Profile = { - ...profileData!, - ...newProfileData, - user_id: placeholderUserId, // Using placeholder user ID for now - }; - - try { - const updatedProfile = await upsertProfile(profileToUpdate); - setProfileData(updatedProfile); - } catch (error) { - console.error('Error updating profile:', error); - throw new Error('Error updating profile'); - } - }, - [profileData], - ); + const setProfile = useCallback(async (completeProfile: Profile) => { + try { + const updatedProfile = await upsertProfile(completeProfile); + setProfileData(updatedProfile); + // Update has_plot if necessary by separate logic + } catch (error) { + console.error('Error setting profile:', error); + throw new Error('Error setting profile'); + } + }, []); - useEffect(() => { - loadProfile(); - }, [loadProfile]); + const updateHasPlot = useCallback(async (plotValue: boolean) => { + try { + setHasPlot(plotValue); + } catch (error) { + console.error('Error updating has_plot:', error); + } + }, []); const providerValue = useMemo( () => ({ profileData, profileReady, + has_plot: hasPlot, setProfile, loadProfile, + updateHasPlot, // Ensure this is included }), - [profileData, profileReady, setProfile, loadProfile], + [ + profileData, + profileReady, + hasPlot, + setProfile, + loadProfile, + updateHasPlot, + ], ); return ( @@ -96,4 +105,4 @@ export const ProfileProvider: React.FC<{ children: ReactNode }> = ({ {children} ); -}; +} From 20fa743a83cd025aadc9be214593da6f3b17296b Mon Sep 17 00:00:00 2001 From: kevin3656 Date: Sun, 3 Nov 2024 17:11:45 -0800 Subject: [PATCH 12/16] addressed lint issues --- utils/ProfileProvider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/ProfileProvider.tsx b/utils/ProfileProvider.tsx index d51f5fc..8f92707 100644 --- a/utils/ProfileProvider.tsx +++ b/utils/ProfileProvider.tsx @@ -5,7 +5,6 @@ import React, { ReactNode, useCallback, useContext, - useEffect, useMemo, useState, } from 'react'; From a2df6d7942fb3ac0e7502d90be83060049ccd99a Mon Sep 17 00:00:00 2001 From: Catherine Tan Date: Mon, 11 Nov 2024 21:45:31 -0800 Subject: [PATCH 13/16] disable next, refactor props and rename to us_state on onboarding --- app/onboarding/page.tsx | 133 ++++++++++++++++++++-------------------- types/schema.d.ts | 2 +- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx index 88711bb..34087e9 100644 --- a/app/onboarding/page.tsx +++ b/app/onboarding/page.tsx @@ -3,31 +3,31 @@ import React, { useState } from 'react'; import { UUID } from 'crypto'; import { Profile, UserTypeEnum } from '@/types/schema'; -import ProfileProvider, { useProfile } from '@/utils/ProfileProvider'; +import { useProfile } from '@/utils/ProfileProvider'; // Define the possible options for each question -const states = ['Tennessee', 'Missouri']; +const states = ['TENNESSEE', 'MISSOURI']; const gardenTypes: UserTypeEnum[] = ['INDIV', 'SCHOOL', 'ORG']; const plotOptions = [ - { label: 'yes', value: true }, - { label: 'no', value: false }, + { label: 'Yes', value: true }, + { label: 'No', value: false }, ]; -const placeholderUserId: UUID = '2abd7296-374a-42d1-bb4f-b813da1615ae'; +const placeholderUserId: UUID = '0802d796-ace8-480d-851b-d16293c74a21'; //Interfaces and props to avoid typ errors on Lint interface StateSelectionProps { - selectedState: string; - setSelectedState: React.Dispatch>; + selectedState?: string; + setSelectedState: (selected: string) => void; } -interface GardenTypeSelectionProps { - selectedGardenType: UserTypeEnum; - setSelectedGardenType: React.Dispatch>; +interface UserTypeSelectionProps { + selectedUserType?: UserTypeEnum; + setSelectedUserType: (selected: UserTypeEnum) => void; } interface PlotSelectionProps { - selectedPlot: boolean | null; - setSelectedPlot: React.Dispatch>; + selectedPlot?: boolean; + setSelectedPlot: (selected: boolean) => void; } // Select State @@ -55,12 +55,12 @@ const StateSelection = ({ ); }; -// Step 2: Select garden type +// Step 2: Select user type -const GardenTypeSelection = ({ - selectedGardenType, - setSelectedGardenType, -}: GardenTypeSelectionProps) => { +const UserTypeSelection = ({ + selectedUserType, + setSelectedUserType, +}: UserTypeSelectionProps) => { return (

What type of garden do you want to create?

@@ -70,10 +70,8 @@ const GardenTypeSelection = ({ type="radio" name="gardenType" value={type} - checked={selectedGardenType === type} - onChange={e => - setSelectedGardenType(e.target.value as UserTypeEnum) - } + checked={selectedUserType === type} + onChange={e => setSelectedUserType(e.target.value as UserTypeEnum)} /> {type} @@ -96,7 +94,11 @@ const PlotSelection = ({ type="radio" name="plot" value={String(option.value)} - checked={selectedPlot === option.value} + checked={ + typeof selectedPlot === 'undefined' + ? false + : selectedPlot === option.value + } onChange={() => setSelectedPlot(option.value)} /> {option.label} @@ -107,13 +109,16 @@ const PlotSelection = ({ }; // Main Onboarding Component -const OnboardingFlow = () => { +export default function OnboardingFlow() { const { setProfile, updateHasPlot } = useProfile(); const [step, setStep] = useState(1); const [selectedState, setSelectedState] = useState(''); - const [selectedGardenType, setSelectedGardenType] = - useState('INDIV'); - const [selectedPlot, setSelectedPlot] = useState(false); + const [selectedUserType, setSelectedUserType] = useState< + UserTypeEnum | undefined + >(undefined); + const [selectedPlot, setSelectedPlot] = useState( + undefined, + ); const handleNext = () => { setStep(step + 1); @@ -126,55 +131,53 @@ const OnboardingFlow = () => { try { const profileToUpload: Profile = { user_id: placeholderUserId, - state: selectedState, - user_type: selectedGardenType, + us_state: selectedState, + user_type: selectedUserType!, }; await setProfile(profileToUpload); - await updateHasPlot(selectedPlot); // Update has_plot - console.log('Profile and has_plot updated successfully'); + await updateHasPlot(selectedPlot!); // Update has_plot } catch (error) { console.error('Error submitting profile:', error); } }; - // Handle form submission, e.g., send to a server or display a confirmation + + const disableNext = () => { + if (step === 1) return !selectedState; + if (step === 2) return !selectedUserType; + if (step === 3) return !(typeof selectedPlot === 'undefined'); + }; return ( - +
+ {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + )} +
- {step === 1 && ( - + {step > 1 && } + {step < 3 && ( + )} - {step === 2 && ( - - )} - {step === 3 && ( - - )} - -
- {step > 1 && } - {step < 3 && ( - - )} - {step === 3 && } -
+ {step === 3 && }
- +
); -}; -export default OnboardingFlow; +} diff --git a/types/schema.d.ts b/types/schema.d.ts index 24ba33c..1882b95 100644 --- a/types/schema.d.ts +++ b/types/schema.d.ts @@ -6,7 +6,7 @@ export type DifficultyLevelEnum = 'EASY' | 'MODERATE' | 'HARD'; export interface Profile { user_id: UUID; - state: string; + us_state: string; user_type: UserTypeEnum; } From 1e8f9f7747e9aae29d0c4fdb9bd794d04e289596 Mon Sep 17 00:00:00 2001 From: Catherine Tan Date: Mon, 11 Nov 2024 21:46:20 -0800 Subject: [PATCH 14/16] disable next, refactor props and rename to us_state on onboarding --- utils/ProfileProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/ProfileProvider.tsx b/utils/ProfileProvider.tsx index 8f92707..b12a69b 100644 --- a/utils/ProfileProvider.tsx +++ b/utils/ProfileProvider.tsx @@ -47,7 +47,7 @@ export default function ProfileProvider({ try { const profile: Profile = { user_id: placeholderUserId, - state: '', + us_state: '', user_type: 'INDIV', //default for now // Removed has_plot as it's not part of Profile }; From 189f444753ee8c64ae4ab316ff09898005c669db Mon Sep 17 00:00:00 2001 From: Catherine Tan Date: Mon, 11 Nov 2024 22:49:02 -0800 Subject: [PATCH 15/16] add fetchProfile query; rename to setHasPlot --- api/supabase/queries/profiles.ts | 14 ++++++++ app/onboarding/page.tsx | 4 +-- hooks/useProfile.ts | 31 ----------------- utils/ProfileProvider.tsx | 57 ++++++++++---------------------- 4 files changed, 33 insertions(+), 73 deletions(-) delete mode 100644 hooks/useProfile.ts diff --git a/api/supabase/queries/profiles.ts b/api/supabase/queries/profiles.ts index 3b54de8..f4ce965 100644 --- a/api/supabase/queries/profiles.ts +++ b/api/supabase/queries/profiles.ts @@ -1,3 +1,4 @@ +import { UUID } from 'crypto'; import { Profile } from '@/types/schema'; import supabase from '../createClient'; @@ -12,3 +13,16 @@ export async function upsertProfile(profile: Profile) { return data; } + +export async function fetchProfileById(userId: UUID) { + const { data, error } = await supabase + .from('profiles') + .select('*') + .eq('user_id', userId) + .single(); + + if (error) + throw new Error(`Error fetching profile id ${userId}: ${error.message}`); + + return data; +} diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx index 34087e9..6d57aba 100644 --- a/app/onboarding/page.tsx +++ b/app/onboarding/page.tsx @@ -110,7 +110,7 @@ const PlotSelection = ({ // Main Onboarding Component export default function OnboardingFlow() { - const { setProfile, updateHasPlot } = useProfile(); + const { setProfile, setHasPlot } = useProfile(); const [step, setStep] = useState(1); const [selectedState, setSelectedState] = useState(''); const [selectedUserType, setSelectedUserType] = useState< @@ -136,7 +136,7 @@ export default function OnboardingFlow() { }; await setProfile(profileToUpload); - await updateHasPlot(selectedPlot!); // Update has_plot + await setHasPlot(selectedPlot!); // Update has_plot } catch (error) { console.error('Error submitting profile:', error); } diff --git a/hooks/useProfile.ts b/hooks/useProfile.ts deleted file mode 100644 index 3543922..0000000 --- a/hooks/useProfile.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useState } from 'react'; -import { Profile } from '../types/schema'; - -const initialProfiles: Profile[] = []; - -export const useProfile = () => { - const [profiles, setProfiles] = useState(initialProfiles); - - const addProfile = (newProfile: Profile) => { - setProfiles(prev => [...prev, newProfile]); - }; - - const updateProfile = (index: number, updates: Partial) => { - setProfiles(prev => - prev.map((profile, i) => - i === index ? { ...profile, ...updates } : profile, - ), - ); - }; - - const removeProfile = (index: number) => { - setProfiles(prev => prev.filter((_, i) => i !== index)); - }; - - return { - profiles, - addProfile, - updateProfile, - removeProfile, - }; -}; diff --git a/utils/ProfileProvider.tsx b/utils/ProfileProvider.tsx index b12a69b..7b721e4 100644 --- a/utils/ProfileProvider.tsx +++ b/utils/ProfileProvider.tsx @@ -8,22 +8,26 @@ import React, { useMemo, useState, } from 'react'; -import { upsertProfile } from '@/api/supabase/queries/profiles'; +import { + fetchProfileById, + upsertProfile, +} from '@/api/supabase/queries/profiles'; import { Profile } from '@/types/schema'; // Define a placeholder user ID for development purposes -const placeholderUserId = '2abd7296-374a-42d1-bb4f-b813da1615ae'; +const placeholderUserId = '0802d796-ace8-480d-851b-d16293c74a21'; export interface ProfileContextType { profileData: Profile | null; profileReady: boolean; - has_plot: boolean; + has_plot: boolean | null; setProfile: (completeProfile: Profile) => Promise; // Now expects full Profile loadProfile: () => Promise; - updateHasPlot: (plotValue: boolean) => Promise; // Add this line + setHasPlot: (plotValue: boolean | null) => void; } const ProfileContext = createContext(undefined); + export const useProfile = () => { const context = useContext(ProfileContext); if (!context) { @@ -35,30 +39,18 @@ export const useProfile = () => { interface ProfileProviderProps { children: ReactNode; } -export default function ProfileProvider({ - children, -}: ProfileProviderProps): JSX.Element { +export default function ProfileProvider({ children }: ProfileProviderProps) { const [profileData, setProfileData] = useState(null); const [profileReady, setProfileReady] = useState(false); - const [hasPlot, setHasPlot] = useState(false); + const [hasPlot, setHasPlot] = useState(null); const loadProfile = useCallback(async () => { setProfileReady(false); - try { - const profile: Profile = { - user_id: placeholderUserId, - us_state: '', - user_type: 'INDIV', //default for now - // Removed has_plot as it's not part of Profile - }; - const fetchedProfile = await upsertProfile(profile); - setProfileData(fetchedProfile); - // Set has_plot independently, assuming it needs separate handling - } catch (error) { - console.error('Error loading profile:', error); - } finally { - setProfileReady(true); - } + + const fetchedProfile = await fetchProfileById(placeholderUserId); + + setProfileData(fetchedProfile); + setProfileReady(true); }, []); const setProfile = useCallback(async (completeProfile: Profile) => { @@ -72,14 +64,6 @@ export default function ProfileProvider({ } }, []); - const updateHasPlot = useCallback(async (plotValue: boolean) => { - try { - setHasPlot(plotValue); - } catch (error) { - console.error('Error updating has_plot:', error); - } - }, []); - const providerValue = useMemo( () => ({ profileData, @@ -87,16 +71,9 @@ export default function ProfileProvider({ has_plot: hasPlot, setProfile, loadProfile, - updateHasPlot, // Ensure this is included + setHasPlot, }), - [ - profileData, - profileReady, - hasPlot, - setProfile, - loadProfile, - updateHasPlot, - ], + [profileData, profileReady, hasPlot, setProfile, loadProfile, setHasPlot], ); return ( From 850dc4ffd974c32f396834f400788479f34802dd Mon Sep 17 00:00:00 2001 From: Catherine Tan Date: Tue, 12 Nov 2024 00:39:45 -0800 Subject: [PATCH 16/16] add useEffect to loadProfile --- utils/ProfileProvider.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/utils/ProfileProvider.tsx b/utils/ProfileProvider.tsx index 7b721e4..13a63ff 100644 --- a/utils/ProfileProvider.tsx +++ b/utils/ProfileProvider.tsx @@ -5,6 +5,7 @@ import React, { ReactNode, useCallback, useContext, + useEffect, useMemo, useState, } from 'react'; @@ -20,7 +21,7 @@ const placeholderUserId = '0802d796-ace8-480d-851b-d16293c74a21'; export interface ProfileContextType { profileData: Profile | null; profileReady: boolean; - has_plot: boolean | null; + hasPlot: boolean | null; setProfile: (completeProfile: Profile) => Promise; // Now expects full Profile loadProfile: () => Promise; setHasPlot: (plotValue: boolean | null) => void; @@ -53,6 +54,10 @@ export default function ProfileProvider({ children }: ProfileProviderProps) { setProfileReady(true); }, []); + useEffect(() => { + loadProfile(); + }, [loadProfile]); + const setProfile = useCallback(async (completeProfile: Profile) => { try { const updatedProfile = await upsertProfile(completeProfile); @@ -68,7 +73,7 @@ export default function ProfileProvider({ children }: ProfileProviderProps) { () => ({ profileData, profileReady, - has_plot: hasPlot, + hasPlot: hasPlot, setProfile, loadProfile, setHasPlot,