diff --git a/api/supabase/queries/userPlants.ts b/api/supabase/queries/userPlants.ts index ce60a61..dc3e398 100644 --- a/api/supabase/queries/userPlants.ts +++ b/api/supabase/queries/userPlants.ts @@ -17,6 +17,7 @@ export async function insertUserPlants( if (error) throw new Error(`Error inserting data: ${error.message}`); }); } + export async function getUserPlantById(userPlantId: UUID): Promise { const { data, error } = await supabase .from('user_plants') @@ -27,10 +28,11 @@ export async function getUserPlantById(userPlantId: UUID): Promise { .single(); if (error) { - throw new Error(`Error fetching plant ID: ${error}`); + throw new Error(`Error fetching plant ID ${userPlantId}: ${error.message}`); } return data; } + export async function getCurrentUserPlantsByUserId( user_id: UUID, ): Promise { @@ -39,8 +41,36 @@ export async function getCurrentUserPlantsByUserId( .select('*') .eq('user_id', user_id) .is('date_removed', null); + + if (error) { + throw new Error( + `Error fetching userPlants for user ${user_id}: ${error.message}`, + ); + } + return data; +} + +export async function upsertUserPlant(userPlant: UserPlant) { + const { data, error } = await supabase + .from('user_plants') + .upsert(userPlant) + .select(); + + if (error) { + throw new Error(`Error upserting plant ${userPlant.id}: ${error.message}`); + } + return data; +} + +// removeUserPlantById is not currenlty being used +export async function removeUserPlantById(id: UUID) { + const { data, error } = await supabase + .from('user_plants') + .delete() + .eq('id', id); + if (error) { - throw new Error(`Error fetching userPlant: ${error}`); + throw new Error(`Error deleting plant ${id}:' ${error}`); } return data; } diff --git a/app/add-details/page.tsx b/app/add-details/page.tsx index c26a79f..2183a0a 100644 --- a/app/add-details/page.tsx +++ b/app/add-details/page.tsx @@ -88,8 +88,9 @@ export default function Home() { function disableNext() { // disable next if planting type is "SELECT" or undefined return !( - details[currentIndex - 1].planting_type && - details[currentIndex - 1].planting_type !== 'SELECT' + details[currentIndex - 1].planting_type + // requires refactor of details to ensure that planting_type is PlantingTypeEnum + // && details[currentIndex - 1].planting_type !== 'SELECT' ); } diff --git a/app/all-plants/[plantId]/page.tsx b/app/all-plants/[plantId]/page.tsx deleted file mode 100644 index ce975f4..0000000 --- a/app/all-plants/[plantId]/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; -import { UUID } from 'crypto'; -import { getPlantById } from '@/api/supabase/queries/plants'; -import { Plant } from '@/types/schema'; - -export default function GeneralPlantPage() { - const params = useParams(); - const plantId: UUID = params.plantId as UUID; - const [currentPlant, setCurrentPlant] = useState(); - useEffect(() => { - const getPlant = async () => { - const plant = await getPlantById(plantId); - setCurrentPlant(plant); - }; - getPlant(); - }, [plantId]); - return ( -
- {currentPlant && ( -
-

{JSON.stringify(currentPlant)}

-
- )} -
- ); -} diff --git a/app/my-garden/[userPlantId]/page.tsx b/app/my-garden/[userPlantId]/page.tsx deleted file mode 100644 index 9fd4a44..0000000 --- a/app/my-garden/[userPlantId]/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useParams } from 'next/navigation'; -import { UUID } from 'crypto'; -import { getMatchingPlantForUserPlant } from '@/api/supabase/queries/plants'; -import { getUserPlantById } from '@/api/supabase/queries/userPlants'; -import { Plant, UserPlant } from '@/types/schema'; - -export default function UserPlantPage() { - const params = useParams(); - const userPlantId: UUID = params.userPlantId as UUID; - const [currentPlant, setCurrentPlant] = useState(); - const [currentUserPlant, setCurrentUserPlant] = useState(); - useEffect(() => { - const getPlant = async () => { - const userPlant = await getUserPlantById(userPlantId); - setCurrentUserPlant(userPlant); - const plant = await getMatchingPlantForUserPlant(userPlant); - setCurrentPlant(plant); - }; - getPlant(); - }, [userPlantId]); - return ( -
- {currentPlant && ( -
-

{JSON.stringify(currentUserPlant)}

-

{JSON.stringify(currentPlant)}

-
- )} -
- ); -} diff --git a/app/plant-page/all-plants/[plantId]/page.tsx b/app/plant-page/all-plants/[plantId]/page.tsx new file mode 100644 index 0000000..da0c4c8 --- /dev/null +++ b/app/plant-page/all-plants/[plantId]/page.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { UUID } from 'crypto'; +import { getPlantById } from '@/api/supabase/queries/plants'; +import DifficultyLevelBar from '@/components/DifficultyLevelBar'; +import GardeningTips from '@/components/GardeningTips'; +import PlantCalendarRow from '@/components/PlantCalendarRow'; +import PlantCareDescription from '@/components/PlantCareDescription'; +import { Flex } from '@/styles/containers'; +import { H4 } from '@/styles/text'; +import { Plant } from '@/types/schema'; +import { + BackButton, + ButtonWrapper, + ComponentWrapper, + Content, + ImgHeader, + NameWrapper, + PlantImage, + PlantName, +} from '../../style'; +import { AddPlant } from './style'; + +export default function GeneralPlantPage() { + const router = useRouter(); + + const params = useParams(); + const plantId: UUID = params.plantId as UUID; + const [currentPlant, setCurrentPlant] = useState(); + useEffect(() => { + const getPlant = async () => { + const plant = await getPlantById(plantId); + setCurrentPlant(plant); + }; + getPlant(); + }, [plantId]); + return currentPlant ? ( + <> + + + { + router.push(`/view-plants`); + }} + > + ← + + + + + + + + {currentPlant.plant_name} + + + Add + + + + + + + +

Planting Timeline

+ {/*add SeasonalColorKey here */} + +
+
+
+ + ) : ( + <>Loading + ); +} diff --git a/app/plant-page/all-plants/[plantId]/style.ts b/app/plant-page/all-plants/[plantId]/style.ts new file mode 100644 index 0000000..7bab1cb --- /dev/null +++ b/app/plant-page/all-plants/[plantId]/style.ts @@ -0,0 +1,81 @@ +import styled from 'styled-components'; +import COLORS from '@/styles/colors'; + +export const Container = styled.div` + padding: 20px; +`; + +export const Header = styled.div` + background-color: ${COLORS.backgroundGrey}; + margin: -28px -28px 24px -28px; + padding: 12px 20px; + padding-bottom: 27px; +`; + +export const HeaderContent = styled.div` + position: relative; + max-width: 100%; +`; + +export const ButtonWrapper = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + padding-top: 24px; +`; + +export const BackButton = styled.button` + background: none; + border: none; + font-size: 1.125rem; + cursor: pointer; + padding: 0; +`; + +export const PlantImage = styled.img` + max-width: 150px; + height: auto; + margin: 9px auto 0; + display: block; +`; + +export const Content = styled.div` + display: flex; + flex-direction: column; + gap: 20px; +`; + +export const NameWrapper = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + gap: 8.5px; +`; + +export const PlantName = styled.h1` + text-align: center; + font-size: 1.5rem; + font-style: normal; + font-weight: 400; + line-height: normal; + margin: 0; +`; + +export const TitleWrapper = styled.div` + display: flex; + flex-direction: row; + gap: 2px; + justify-content: space-between; +`; +export const AddPlant = styled.button` + background-color: ${COLORS.shrub}; + color: white; + padding: 8px 16px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 0.75rem; + font-style: inherit; + font-weight: 400; + line-height: normal; +`; diff --git a/app/plant-page/my-garden/[userPlantId]/page.tsx b/app/plant-page/my-garden/[userPlantId]/page.tsx new file mode 100644 index 0000000..790465f --- /dev/null +++ b/app/plant-page/my-garden/[userPlantId]/page.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { UUID } from 'crypto'; +import { getMatchingPlantForUserPlant } from '@/api/supabase/queries/plants'; +import { + getUserPlantById, + upsertUserPlant, +} from '@/api/supabase/queries/userPlants'; +import DifficultyLevelBar from '@/components/DifficultyLevelBar'; +import GardeningTips from '@/components/GardeningTips'; +import PlantCalendarRow from '@/components/PlantCalendarRow'; +import PlantCareDescription from '@/components/PlantCareDescription'; +import YourPlantDetails from '@/components/YourPlantDetails'; +import { Flex } from '@/styles/containers'; +import { H4 } from '@/styles/text'; +import { Plant, UserPlant } from '@/types/schema'; +import { getCurrentTimestamp } from '@/utils/helpers'; +import { + BackButton, + ButtonWrapper, + ComponentWrapper, + Content, + ImgHeader, + NameWrapper, + PlantImage, + PlantName, +} from '../../style'; +import { RemoveButton, Subtitle } from './style'; + +export default function UserPlantPage() { + const router = useRouter(); + const params = useParams(); + const userPlantId: UUID = params.userPlantId as UUID; + const [currentPlant, setCurrentPlant] = useState(); + const [currentUserPlant, setCurrentUserPlant] = useState(); + + useEffect(() => { + const getPlant = async () => { + const userPlant = await getUserPlantById(userPlantId); + setCurrentUserPlant(userPlant); + const plant = await getMatchingPlantForUserPlant(userPlant); + setCurrentPlant(plant); + }; + getPlant(); + }, [userPlantId]); + + async function removePlant() { + if (!currentUserPlant) return; + try { + // Originally, removeUserPlantById(userPlantId); + currentUserPlant.date_removed = getCurrentTimestamp(); + await upsertUserPlant(currentUserPlant); + } catch (error) { + console.error(error); + } + router.push(`/view-plants`); + } + + return currentPlant && currentUserPlant ? ( + <> + + + { + router.push(`/view-plants`); + }} + > + ← + + X Remove + + + + + + + + {currentPlant.plant_name} + + + You have this plant in your garden! + + + + + + + + + +

Planting Timeline

+ {/*add SeasonalColorKey here */} + +
+
+
+ + ) : ( + <>Loading + ); +} diff --git a/app/plant-page/my-garden/[userPlantId]/style.ts b/app/plant-page/my-garden/[userPlantId]/style.ts new file mode 100644 index 0000000..b0351d7 --- /dev/null +++ b/app/plant-page/my-garden/[userPlantId]/style.ts @@ -0,0 +1,24 @@ +import styled from 'styled-components'; +import COLORS from '@/styles/colors'; +import { P3 } from '@/styles/text'; + +// only for UserPlantPage +export const RemoveButton = styled.button` + background-color: ${COLORS.shrub}; + color: white; + padding: 8px 16px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 0.75rem; + font-style: inherit; + font-weight: 400; + line-height: normal; +`; + +// Only for UserPlantPage +export const Subtitle = styled(P3)` + font-style: italic; + font-weight: 400; + color: ${COLORS.shrub}; +`; diff --git a/app/plant-page/style.ts b/app/plant-page/style.ts new file mode 100644 index 0000000..e11fbec --- /dev/null +++ b/app/plant-page/style.ts @@ -0,0 +1,63 @@ +import styled from 'styled-components'; +import COLORS from '@/styles/colors'; +import { H3 } from '@/styles/text'; + +// Image Header +export const ImgHeader = styled.div` + display: flex; + background-color: ${COLORS.backgroundGrey}; + padding-bottom: 24px; + position: relative; + height: 220px; +`; + +export const ButtonWrapper = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + position: absolute; + top: 24px; + padding-left: 24px; + padding-right: 24px; +`; + +export const BackButton = styled.button` + background: none; + border: none; + font-size: 1.125rem; + cursor: pointer; + padding: 0; +`; + +export const PlantImage = styled.img` + align-self: center; + max-width: 150px; + height: auto; + margin: 9px auto 0; + display: block; +`; + +// Content +export const Content = styled.div` + display: flex; + flex-direction: column; + padding: 24px; +`; + +// Title Section +export const PlantName = styled(H3)` + text-align: center; + font-weight: 400; +`; + +export const NameWrapper = styled.div` + display: flex; + gap: 8px; + align-items: center; +`; + +export const ComponentWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 32px; +`; diff --git a/app/view-plants/page.tsx b/app/view-plants/page.tsx index 971fdad..9129167 100644 --- a/app/view-plants/page.tsx +++ b/app/view-plants/page.tsx @@ -156,7 +156,7 @@ export default function Page() { ]); function handleUserPlantCardClick(ownedPlant: OwnedPlant) { - router.push(`my-garden/${ownedPlant.userPlantId}`); + router.push(`/plant-page/my-garden/${ownedPlant.userPlantId}`); } function handlePlantCardClick(plant: Plant) { @@ -167,7 +167,7 @@ export default function Page() { setSelectedPlants([...selectedPlants, plant]); } } else { - router.push(`all-plants/${plant.id}`); + router.push(`/plant-page/all-plants/${plant.id}`); } } function handleAddPlants() { diff --git a/components/Button.tsx b/components/Button.tsx index 1d22cc0..d2b038f 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -22,8 +22,9 @@ interface SmallRoundedButtonProps { export const SmallRoundedButton = styled.button` padding: 10px 20px; - border-radius: 50px; - border: 2px solid ${({ $secondaryColor }) => $secondaryColor}; + border-radius: 15px; + box-shadow: 1px 1px 1px 0px rgba(0, 0, 0, 0.05); + border: 0.5px solid ${({ $secondaryColor }) => $secondaryColor}; background-color: ${({ $primaryColor }) => $primaryColor ? $primaryColor : 'white'}; color: ${({ $primaryColor, $secondaryColor }) => diff --git a/components/GardeningTips/index.tsx b/components/GardeningTips/index.tsx index 4ae34b7..1de0563 100644 --- a/components/GardeningTips/index.tsx +++ b/components/GardeningTips/index.tsx @@ -1,7 +1,7 @@ 'use client'; import COLORS from '@/styles/colors'; -import { H3 } from '@/styles/text'; +import { P1, P3 } from '@/styles/text'; import Icon from '../Icon'; import { Container, IconWrapper, TipsList, TitleWrapper } from './style'; @@ -20,11 +20,15 @@ export default function GardeningTips({ -

Gardening Tips for {plantName}

+ + Gardening Tips for {plantName} + {tipsArray.map((tip, index) => ( -
  • {tip.trim()}
  • + + {tip.trim()} + ))}
    diff --git a/components/GardeningTips/style.ts b/components/GardeningTips/style.ts index 2a0f09c..7f88f1e 100644 --- a/components/GardeningTips/style.ts +++ b/components/GardeningTips/style.ts @@ -1,14 +1,14 @@ import styled from 'styled-components'; +import COLORS from '@/styles/colors'; export const Container = styled.div` - background-color: #f9f9f9; - padding: 1.5rem; + background-color: ${COLORS.backgroundGrey}; + padding: 16px; border-radius: 8px; display: flex; flex-direction: column; align-items: center; text-align: center; - color: #333; `; export const TitleWrapper = styled.div` @@ -19,13 +19,9 @@ export const TitleWrapper = styled.div` export const IconWrapper = styled.span` margin-right: 6px; - color: #8bc34a; + color: ${COLORS.shrub}; `; export const TipsList = styled.ol` - margin: 0; - padding-left: 0; - font-size: 1rem; - line-height: 1.5; list-style-position: inside; `; diff --git a/components/PlantCard/index.tsx b/components/PlantCard/index.tsx index 5d328ae..28bb290 100644 --- a/components/PlantCard/index.tsx +++ b/components/PlantCard/index.tsx @@ -60,7 +60,7 @@ const PlantCard = memo(function PlantCard({ - + {plant.water_frequency} diff --git a/components/PlantCareDescription.tsx b/components/PlantCareDescription.tsx new file mode 100644 index 0000000..c08485b --- /dev/null +++ b/components/PlantCareDescription.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { IconType } from '@/lib/icons'; +import { Flex } from '@/styles/containers'; +import { H4, P3 } from '@/styles/text'; +import { displaySunlightEnumFromHours } from '@/utils/helpers'; +import Icon from './Icon'; + +function IconRow(iconType: IconType, boldText: string, text: string) { + return ( + + + + + {boldText} + {' '} + {text} + + + ); +} + +export default function PlantCareDescription({ + waterFreq, + weedingFreq, + sunlightMinHours, + sunlightMaxHours, +}: { + waterFreq: string; + weedingFreq: string; + sunlightMinHours: number; + sunlightMaxHours: number; +}) { + const sunlightText = `${sunlightMinHours} ${sunlightMaxHours ? ` - ${sunlightMaxHours}` : ''} hours (${displaySunlightEnumFromHours(sunlightMinHours)})`; + return ( + +

    Plant Description

    + + {IconRow('wateringCan', 'Watering Frequency:', waterFreq)} + {IconRow('wateringCan', 'Weeding Frequency:', weedingFreq)} + {IconRow('sun', 'Sunlight Requirement:', sunlightText)} + +
    + ); +} diff --git a/components/PlantCareDescription/index.tsx b/components/PlantCareDescription/index.tsx deleted file mode 100644 index 6d4b019..0000000 --- a/components/PlantCareDescription/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import Icon from '../Icon'; -import { CareItem, CareText, Container, IconWrapper, Title } from './style'; - -export default function PlantCareDescription({ - waterFreq, - weedingFreq, - sunlightMinHours, - sunlightMaxHours, -}: { - waterFreq: string; - weedingFreq: string; - sunlightMinHours: number; - sunlightMaxHours: number; -}) { - return ( - - Plant Description - - - - - - Water Frequency: {waterFreq} - - - - - - - - Weeding Frequency: {weedingFreq} - - - - - - - - Sunlight Requirement: {sunlightMinHours}- - {sunlightMaxHours} hours (Full Sun) - - - - ); -} diff --git a/components/PlantCareDescription/style.ts b/components/PlantCareDescription/style.ts deleted file mode 100644 index cbb935f..0000000 --- a/components/PlantCareDescription/style.ts +++ /dev/null @@ -1,25 +0,0 @@ -import styled from 'styled-components'; - -export const Container = styled.div` - color: #333; -`; - -export const Title = styled.h2` - font-size: 1.5rem; - margin-bottom: 1rem; -`; - -export const CareItem = styled.div` - display: flex; - align-items: center; - margin-bottom: 10px; -`; - -export const IconWrapper = styled.div` - margin-right: 6px; - color: #8bc34a; -`; - -export const CareText = styled.span` - font-size: 1rem; -`; diff --git a/components/YourPlantDetails/index.tsx b/components/YourPlantDetails/index.tsx index 47f6245..8ccd441 100644 --- a/components/YourPlantDetails/index.tsx +++ b/components/YourPlantDetails/index.tsx @@ -1,20 +1,27 @@ 'use client'; +import { IconType } from '@/lib/icons'; import COLORS from '@/styles/colors'; -import { H3 } from '@/styles/text'; +import { Flex } from '@/styles/containers'; +import { P1, P3 } from '@/styles/text'; import { PlantingTypeEnum } from '@/types/schema'; import { formatTimestamp, useTitleCase } from '@/utils/helpers'; import Icon from '../Icon'; import { Container, - DetailRow, - DetailsContainer, - DetailText, - EditButton, Header, - StyledIcon, + // EditButton, } from './style'; +function DetailRow(iconType: IconType, text: string) { + return ( + + + {text} + + ); +} + export default function YourPlantDetails({ datePlanted, plantingType, @@ -27,35 +34,20 @@ export default function YourPlantDetails({ return (
    -

    Your Plant Details

    - Edit + + Your Plant Details + + {/* Edit */}
    - - - - - - Date Planted: {formatTimestamp(datePlanted)} - - - - - - - Planting Type: {useTitleCase(plantingType)} - - - {recentHarvestDate && ( - - - - - - Most Recent Harvest Date: {formatTimestamp(recentHarvestDate)} - - - )} - + + {DetailRow('calendar', `Date Planted: ${formatTimestamp(datePlanted)}`)} + {DetailRow('plantHand', `Planting Type: ${useTitleCase(plantingType)}`)} + {recentHarvestDate && + DetailRow( + 'plant', + `Most Recent Harvest Date: ${formatTimestamp(recentHarvestDate)}`, + )} +
    ); } diff --git a/components/YourPlantDetails/style.ts b/components/YourPlantDetails/style.ts index db8e9b6..9c18f84 100644 --- a/components/YourPlantDetails/style.ts +++ b/components/YourPlantDetails/style.ts @@ -12,31 +12,14 @@ export const Header = styled.div` display: flex; justify-content: space-between; align-items: center; - margin-bottom: 18px; + margin-bottom: 16px; `; +// Not Used Yet export const EditButton = styled(SmallRoundedButton)` font-size: 0.875rem; padding: 0.25rem 0.5rem; -`; -export const DetailsContainer = styled.div` - display: flex; - flex-direction: column; - gap: 6px; -`; - -export const DetailRow = styled.div` - display: flex; - align-items: center; - gap: 6px; -`; - -export const DetailText = styled.span` - color: ${COLORS.black}; -`; - -export const StyledIcon = styled.div` - color: ${COLORS.shrub}; - display: flex; - align-items: center; + font-style: normal; + font-weight: 400; + line-height: normal; `; diff --git a/lib/icons.tsx b/lib/icons.tsx index ffb6ffd..48dfffb 100644 --- a/lib/icons.tsx +++ b/lib/icons.tsx @@ -27,7 +27,7 @@ export const IconSvgs = { ), - watering_can: ( + wateringCan: ( { diff --git a/utils/helpers.ts b/utils/helpers.ts index 52cefea..f4270c8 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -6,6 +6,10 @@ import { SunlightEnum, } from '@/types/schema'; +export function getCurrentTimestamp(): string { + return new Date().toISOString(); +} + /* Helper function to process late/early month fields Assumes that month is not null. Assumes that the month is a valid month string; @@ -148,6 +152,28 @@ export function checkSearchTerm(searchTerm: string, plant: Plant) { return plant.plant_name.toLowerCase().includes(searchTerm.toLowerCase()); } +/* Maps sunlight hours to SunlightEnum for display. Only considers sunlightMinHours. +Assumes sunlightMinHours between 0-8. SunlightEnum ranges are as follows (left-inclusive): +SHADE: [0, 2), PARTIAL_SUN: [2, 4), PARTIAL_SUN: [4, 6), FULL: [6, infin) +*/ +function mapHoursToSunlightEnum(sunlightMinHours: number): SunlightEnum { + if (sunlightMinHours < 2) return 'SHADE'; + if (sunlightMinHours < 4) return 'PARTIAL_SHADE'; + if (sunlightMinHours < 6) return 'PARTIAL_SUN'; + else return 'FULL'; +} + +const SunlightEnumDisplayMap: Record = { + SHADE: 'Shade', + PARTIAL_SHADE: 'Partial Shade', + PARTIAL_SUN: 'Partial Sun', + FULL: 'Full Sun', +}; + +export function displaySunlightEnumFromHours(sunlightMinHours: number): string { + return SunlightEnumDisplayMap[mapHoursToSunlightEnum(sunlightMinHours)]; +} + export function checkSunlight( sunlightFilterValue: DropdownOption[], plant: Plant,