diff --git a/app/view-plants/page.tsx b/app/view-plants/page.tsx index 8f3adf3..0930bf3 100644 --- a/app/view-plants/page.tsx +++ b/app/view-plants/page.tsx @@ -11,6 +11,9 @@ import { getCurrentUserPlantsByUserId } from '@/api/supabase/queries/userPlants' import FilterDropdownMultiple from '@/components/FilterDropdownMultiple'; import PlantCard from '@/components/PlantCard'; import SearchBar from '@/components/SearchBar'; +import COLORS from '@/styles/colors'; +import { Box, Flex } from '@/styles/containers'; +import { H1 } from '@/styles/text'; import { DropdownOption, OwnedPlant, Plant } from '@/types/schema'; import { checkDifficulty, @@ -18,7 +21,17 @@ import { checkSearchTerm, checkSunlight, } from '@/utils/helpers'; -import { FilterContainer, TopRowContainer } from './styles'; +import { + AddButton, + FilterContainer, + HeaderButton, + NumberSelectedPlants, + NumberSelectedPlantsContainer, + PlantGridContainer, + SelectButton, + TopRowContainer, + ViewSelection, +} from './styles'; export default function Page() { const router = useRouter(); @@ -141,101 +154,145 @@ export default function Page() { router.push(`all-plants/${plant.id}`); } } + function handleAddPlants() { + //TODO: route to add details with proper information + router.push('/add-details'); // use CONFIG later + } + + function handleCancelAddMode() { + setSelectedPlants([]); + setInAddMode(false); + } + + const plantPluralityString = selectedPlants.length > 1 ? 'Plants' : 'Plant'; return ( -
-
-
- - -
-
- - - - - - +
+ +

+ View Plants +

+ + + + + - - -
- {viewingOption === 'myPlants' && ( -
- {filteredUserPlantList.length ? ( -
- {filteredUserPlantList.map(ownedPlant => ( - handleUserPlantCardClick(ownedPlant)} - /> - ))} -
- ) : ( -
- -
- )} -
- )} + + + + + {viewingOption === 'all' && inAddMode ? ( + + + {selectedPlants.length + ? `${selectedPlants.length} ${plantPluralityString} Selected` + : 'Select Plants'} + + + ) : null} + + + + + setViewingOption('myPlants')} + > + My Plants + + setViewingOption('all')} + > + All + + + {/* Select/Cancel toggles Add Mode; appears in All plants only*/} {viewingOption === 'all' && (inAddMode ? ( -
- {filteredPlantList.map((plant, key) => ( - handlePlantCardClick(plant)} - /> - ))} -
- -
-
+ + Cancel + ) : ( -
- {filteredPlantList.map((plant, key) => ( + setInAddMode(true)} + > + Select + + ))} + + {viewingOption === 'myPlants' && ( +
+ {filteredUserPlantList.length ? ( + + {filteredUserPlantList.map(ownedPlant => ( handlePlantCardClick(plant)} + onClick={() => handleUserPlantCardClick(ownedPlant)} + // aspectRatio="168 / 200" /> ))} -
- -
+
+ ) : ( +
+
- ))} -
-
+ )} +
+ )} + {viewingOption === 'all' && ( + <> + + {filteredPlantList.map((plant, key) => ( + handlePlantCardClick(plant)} + // aspectRatio="168 / 200" + /> + ))} + + {inAddMode && ( + + {selectedPlants.length ? 'Add to My Garden' : 'Select Plants'} + + )} + + )} +
); } diff --git a/app/view-plants/styles.ts b/app/view-plants/styles.ts index 03af2dc..d28ffb6 100644 --- a/app/view-plants/styles.ts +++ b/app/view-plants/styles.ts @@ -1,15 +1,98 @@ import styled from 'styled-components'; +import { SmallRoundedButton } from '@/components/Button'; +import COLORS from '@/styles/colors'; export const FilterContainer = styled.div` display: flex; flex-direction: row; gap: 8px; + margin-bottom: 20px; `; export const TopRowContainer = styled.div` display: flex; flex-direction: column; gap: 12px; - margin-top: 8px; - margin-bottom: 8px; + padding-left: 24px; + padding-right: 24px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); +`; + +export const HeaderButton = styled.button<{ + $isCurrentMode: boolean; +}>` + background: none; + border: none; + color: ${COLORS.shrub}; + font-family: inherit; + cursor: pointer; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + color: ${({ $isCurrentMode }) => + $isCurrentMode ? ` ${COLORS.shrub}` : `${COLORS.midgray}`}; + text-decoration: ${({ $isCurrentMode }) => + $isCurrentMode ? ` underline ` : `none`}; +`; + +export const AddButton = styled.button<{ $backgroundColor: string }>` + position: fixed; + + bottom: 0; + background-color: ${({ $backgroundColor }) => $backgroundColor}; + color: white; + border-radius: 20px; + border: none; + font-family: inherit; + margin-bottom: 10px; + width: 170px; + height: 50px; + left: 50%; + transform: translateX(-50%); + font-size: 15px; + font-style: normal; + font-weight: 400; + line-height: normal; +`; + +export const PlantGridContainer = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); + /* grid-template-columns: repeat(auto-fill, 168px); */ + gap: 8px; + max-width: 100%; + justify-content: center; +`; + +export const SelectButton = styled(SmallRoundedButton)` + font-size: 0.75rem; + font-style: normal; + font-weight: 400; + line-height: normal; + width: 60px; + height: 25px; + padding: 0; +`; + +export const ViewSelection = styled.div` + display: flex; + flex-direction: row; + gap: 20px; +`; + +export const NumberSelectedPlantsContainer = styled.div` + background-color: ${COLORS.shrub}; + text-align: center; + width: 100%; + height: 16px; + padding: 0; +`; + +export const NumberSelectedPlants = styled.p` + font-size: 0.625rem; + font-style: normal; + font-weight: 400; + line-height: 16px; + color: #fff; `; diff --git a/components/Button.tsx b/components/Button.tsx index 24f1127..1d22cc0 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -6,8 +6,12 @@ import styled from 'styled-components'; export const Button = React.forwardRef< HTMLButtonElement, React.ButtonHTMLAttributes ->(({ children, ...props }) => { - return ; +>(({ children, ...props }, ref) => { + return ( + + ); }); Button.displayName = 'Button'; diff --git a/components/DifficultyLevelBar/index.tsx b/components/DifficultyLevelBar.tsx similarity index 68% rename from components/DifficultyLevelBar/index.tsx rename to components/DifficultyLevelBar.tsx index 2c362f3..4f33a5d 100644 --- a/components/DifficultyLevelBar/index.tsx +++ b/components/DifficultyLevelBar.tsx @@ -1,7 +1,7 @@ 'use client'; import { DifficultyLevelEnum } from '@/types/schema'; -import Icon from '../Icon'; +import Icon from './Icon'; export default function DifficultyLevelBar({ difficultyLevel, @@ -9,11 +9,11 @@ export default function DifficultyLevelBar({ difficultyLevel: DifficultyLevelEnum; }) { if (difficultyLevel === 'EASY') { - return ; + return ; } else if (difficultyLevel === 'MODERATE') { - return ; + return ; } else { // difficultyLevel === 'HARD' - return ; + return ; } } diff --git a/components/PlantCard/index.tsx b/components/PlantCard/index.tsx index 53e70f2..5d328ae 100644 --- a/components/PlantCard/index.tsx +++ b/components/PlantCard/index.tsx @@ -1,16 +1,23 @@ -import React from 'react'; +import React, { memo } from 'react'; +import { P1 } from '@/styles/text'; import { Plant } from '@/types/schema'; +import { mapMonthToSeason, useTitleCase } from '@/utils/helpers'; +import DifficultyLevelBar from '../DifficultyLevelBar'; +import Icon from '../Icon'; import { Attribute, - Card, + AttributeContent, + CardContainer, CardContent, CardPic, PlantAttributes, + PlantHeader, + PlantImage, RoundCheck, TopRight, } from './styles'; -export default function PlantCard({ +const PlantCard = memo(function PlantCard({ plant, canSelect, isSelected = false, @@ -22,35 +29,54 @@ export default function PlantCard({ onClick?: () => void; }) { return ( - + {canSelect && ( )} - {plant.plant_name} + -

{plant.plant_name}

+ + {plant.plant_name} + + + -

{`${plant.harvest_start} - ${plant.harvest_end}`}

+ + + {useTitleCase( + mapMonthToSeason(plant.outdoors_start) || 'Unknown', + )} + +
+ + + + {useTitleCase(plant.harvest_season)} + -

{plant.water_frequency}

+ + {plant.water_frequency}
-

+ + {plant.sunlight_min_hours} {plant.sunlight_max_hours ? ` - ${plant.sunlight_max_hours}` : ''}{' '} hours/day -

+
-
+ ); -} +}); + +export default PlantCard; diff --git a/components/PlantCard/styles.ts b/components/PlantCard/styles.ts index 42eb286..056d3c7 100644 --- a/components/PlantCard/styles.ts +++ b/components/PlantCard/styles.ts @@ -1,26 +1,33 @@ import styled from 'styled-components'; import COLORS from '@/styles/colors'; -export const Card = styled.div<{ isSelected?: boolean }>` +export const CardContainer = styled.div<{ $isSelected?: boolean }>` position: relative; - height: 40vh; - width: 20vw; + width: 168px; + height: 200px; display: flex; flex-direction: column; align-items: start; border-radius: 12px; background-color: white; - box-shadow: - 0 24px 38px 3px rgba(0, 0, 0, 0.14), - 0 9px 46px 8px rgba(0, 0, 0, 0.12), - 0 11px 15px -7px rgba(0, 0, 0, 0.2); + box-shadow: ${({ $isSelected }) => + $isSelected + ? ` + 0 24px 38px 3px rgb(148, 181, 6, 0.14), + 0 9px 46px 8px rgb(148, 181, 6, 0.12), + 0 11px 15px -7px rgb(148, 181, 6, 0.2)` + : ` + 0 24px 38px 3px rgba(0, 0, 0, 0.14), + 0 9px 46px 8px rgba(0, 0, 0, 0.12), + 0 11px 15px -7px rgba(0, 0, 0, 0.2)`}; - border: ${({ isSelected }) => - isSelected ? `1px solid ${COLORS.sprout}` : 'none'}; + border: ${({ $isSelected }) => + $isSelected ? `1px solid ${COLORS.sprout}` : '1px solid transparent'}; + backdrop-filter: blur(40px); `; export const CardPic = styled.div` - height: 60%; + height: 92px; width: 100%; background-color: #f5f6f6; display: flex; @@ -33,34 +40,22 @@ export const CardPic = styled.div` export const CardContent = styled.div` display: flex; flex-direction: column; - padding-left: 1vw; - height: 40%; - row-gap: 1vh; - - > * { - margin: 0; - } - - > h2 { - font-size: 1rem; - margin-top: 1vh; - } + padding: 12px 16px 8px 16px; + row-gap: 6px; + width: 100%; `; export const PlantAttributes = styled.div` display: flex; flex-direction: column; - gap: 1vh; + gap: 4px; `; export const Attribute = styled.div` display: flex; flex-direction: row; - - > * { - margin: 0; - font-size: 0.625rem; - } + align-items: center; + gap: 6px; `; export const RoundCheck = styled.input.attrs({ type: 'checkbox' })` @@ -95,3 +90,24 @@ export const TopRight = styled.div` right: 0; padding: 10px 10px; `; + +export const AttributeContent = styled.p` + font-size: 10px; + font-style: normal; + font-weight: 300; + line-height: normal; + margin: 0; +`; + +export const PlantHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +`; + +export const PlantImage = styled.img` + width: 60px; + height: 60px; + font-size: 10px; +`; diff --git a/components/YourPlantDetails/index.tsx b/components/YourPlantDetails/index.tsx index f717f6c..47f6245 100644 --- a/components/YourPlantDetails/index.tsx +++ b/components/YourPlantDetails/index.tsx @@ -40,7 +40,7 @@ export default function YourPlantDetails({ - + Planting Type: {useTitleCase(plantingType)} diff --git a/lib/icons.tsx b/lib/icons.tsx index aa1b7b0..a480153 100644 --- a/lib/icons.tsx +++ b/lib/icons.tsx @@ -135,7 +135,7 @@ export const IconSvgs = { ), - plant_hand: ( + plantHand: ( ), - easy_bar: ( + easyBar: ( ), - moderate_bar: ( + moderateBar: ( ), - hard_bar: ( + hardBar: ( ), + outdoorsSeason: ( + + + + + + + + + + ), + harvestSeason: ( + + + + + + + + + + ), }; export type IconType = keyof typeof IconSvgs; diff --git a/styles/colors.ts b/styles/colors.ts index 454e1e5..89abdca 100644 --- a/styles/colors.ts +++ b/styles/colors.ts @@ -2,16 +2,19 @@ const COLORS = { // background white seed: '#FFFBF2', - //greens + // greens shrub: '#1F5A2A', sprout: '#94B506', sproutLight: '#F4F8E6', - //grey + // grey black: '#222222', darkgray: '#555555', midgray: '#888888', lightgray: '#DDDDDD', backgroundGrey: '#f9fafb', + + // error + errorRed: '#D94E4E', }; export default COLORS; diff --git a/styles/containers.ts b/styles/containers.ts new file mode 100644 index 0000000..de5ca49 --- /dev/null +++ b/styles/containers.ts @@ -0,0 +1,97 @@ +// from IJP's container.ts file +import styled, { css } from 'styled-components'; + +interface BoxProps { + $textAlign?: 'center' | 'start' | 'end' | 'justify'; + $background?: string; + $border?: string; + $borderColor?: string; + $radius?: string; + $shadow?: string; + $w?: string; + $minW?: string; + $maxW?: string; + $h?: string; + $minH?: string; + $maxH?: string; + $p?: string; + $px?: string; + $py?: string; + $pl?: string; + $pr?: string; + $pt?: string; + $pb?: string; + $m?: string; + $mx?: string; + $my?: string; + $ml?: string; + $mt?: string; + $mr?: string; + $mb?: string; + $position?: string; +} + +const BoxStyles = css` + position: ${({ $position }) => $position ?? 'static'}; + text-align: ${({ $textAlign }) => $textAlign}; + + background: ${({ $background }) => $background}; + border: ${({ $border }) => $border}; + border-color: ${({ $borderColor }) => $borderColor}; + border-radius: ${({ $radius }) => $radius}; + box-shadow: ${({ $shadow }) => $shadow}; + + width: ${({ $w }) => $w ?? '100%'}; + min-width: ${({ $minW }) => $minW}; + max-width: ${({ $maxW }) => $maxW}; + height: ${({ $h }) => $h ?? '100%'}; + min-height: ${({ $minH }) => $minH}; + max-height: ${({ $maxH }) => $maxH}; + + padding: ${({ $p }) => $p}; + ${({ $py, $pt }) => ($pt || $py ? `padding-top: ${$pt || $py}` : null)}; + ${({ $py, $pb }) => ($pb || $py ? `padding-bottom: ${$pb || $py}` : null)}; + ${({ $px, $pl }) => ($pl || $px ? `padding-left: ${$pl || $px}` : null)}; + ${({ $px, $pr }) => ($pr || $px ? `padding-right: ${$pr || $px}` : null)}; + + margin: ${({ $m }) => $m}; + ${({ $my, $mt }) => ($mt || $my ? `margin-top: ${$mt || $my}` : null)}; + ${({ $my, $mb }) => ($mb || $my ? `margin-bottom: ${$mb || $my}` : null)}; + ${({ $mx, $ml }) => ($ml || $mx ? `margin-left: ${$ml || $mx}` : null)}; + ${({ $mx, $mr }) => ($mr || $mx ? `margin-right: ${$mr || $mx}` : null)}; +`; + +export const Box = styled.div` + ${BoxStyles} +`; + +type Justify = 'center' | 'start' | 'end' | 'between' | 'evenly'; + +interface FlexProps extends BoxProps { + $gap?: string; + $direction?: 'column' | 'row'; + $align?: 'center' | 'start' | 'end'; + $justify?: Justify; + $wrap?: boolean; +} + +const parseJustify = (justify: Justify) => { + if (justify === 'center') return 'center'; + if (justify === 'start' || justify === 'end') return `flex-${justify}`; + return `space-${justify}`; +}; + +export const Flex = styled.div` + ${BoxStyles} + display: flex; + flex-direction: ${({ $direction }) => $direction || 'row'}; + gap: ${({ $gap }) => $gap || '0px'}; + + align-items: ${({ $align }) => + $align === 'center' ? $align : `flex-${$align}`}; + + justify-content: ${({ $justify }) => + $justify ? parseJustify($justify) : null}; + + flex-wrap: ${({ $wrap }) => ($wrap ? 'wrap' : null)}; +`; diff --git a/utils/helpers.ts b/utils/helpers.ts index 2ae9941..2c9c986 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -1,4 +1,4 @@ -import { DropdownOption, Plant } from '@/types/schema'; +import { DropdownOption, Plant, SeasonEnum } from '@/types/schema'; // Helper function to process late/early month fields for checkGrowingSeason function processPlantMonth(month: string) { @@ -227,3 +227,26 @@ export function formatTimestamp(timestamp: string): string { // Return in MM/DD/YYYY format return `${month}/${day}/${year}`; } + +const monthToSeason: Record = { + JANUARY: 'WINTER', + FEBRUARY: 'WINTER', + MARCH: 'SPRING', + APRIL: 'SPRING', + MAY: 'SPRING', + JUNE: 'SUMMER', + JULY: 'SUMMER', + AUGUST: 'SUMMER', + SEPTEMBER: 'FALL', + OCTOBER: 'FALL', + NOVEMBER: 'FALL', + DECEMBER: 'WINTER', +}; + +/* Maps a month to a season +if valid month (e.g. 'LATE_JANUARY' or 'FEBRUARY'), return SeasonEnum +else return null*/ +export function mapMonthToSeason(month: string): SeasonEnum | null { + month = processPlantMonth(month).toUpperCase(); + return monthToSeason[month] || null; +}