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/layout.tsx b/app/layout.tsx index c0101bc..52056e7 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..7d068ea 100644 --- a/app/onboarding/page.tsx +++ b/app/onboarding/page.tsx @@ -1,37 +1,52 @@ 'use client'; import React, { useState } from 'react'; -import { upsertProfile } from '@/api/supabase/queries/profiles'; -import { Profile } from '@/types/schema'; +import { UUID } from 'crypto'; +import { Profile, UserTypeEnum } from '@/types/schema'; +import { useProfile } from '@/utils/ProfileProvider'; + +type Options = { + label: string; // Represents the text for UI + value: T; // Represents the value stored in the DB +}; // Define the possible options for each question -const states = ['Tennessee', 'Missouri']; -const gardenTypes = ['Individual', 'Community', 'School']; -const plotOptions = [ +const states: Options[] = [ + { label: 'Tennesee', value: 'TENNESSEE' }, + { label: 'Missouri', value: 'MISSOUR' }, +]; +const gardenTypes: Options[] = [ + { label: 'Individual', value: 'INDIV' }, + { label: 'School', value: 'SCHOOL' }, + { label: 'Organization', value: 'ORG' }, +]; +const plotOptions: Options[] = [ { label: 'yes', value: true }, { label: 'no', value: false }, ]; +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: string; - 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 -const StateSelection: React.FC = ({ +const StateSelection = ({ selectedState, setSelectedState, -}) => { +}: StateSelectionProps) => { return (

Which state do you live in?

@@ -43,8 +58,8 @@ const StateSelection: React.FC = ({ Select a state {states.map(state => ( - ))} @@ -52,25 +67,25 @@ const StateSelection: React.FC = ({ ); }; -// Step 2: Select garden type +// Step 2: Select user type -const GardenTypeSelection: React.FC = ({ - selectedGardenType, - setSelectedGardenType, -}) => { +const UserTypeSelection = ({ + selectedUserType, + setSelectedUserType, +}: UserTypeSelectionProps) => { return (

What type of garden do you want to create?

{gardenTypes.map(type => ( -
@@ -78,10 +93,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?

@@ -91,7 +106,11 @@ const PlotSelection: React.FC = ({ 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} @@ -102,11 +121,16 @@ const PlotSelection: React.FC = ({ }; // Main Onboarding Component -const OnboardingFlow = () => { +export default function OnboardingFlow() { + const { setProfile, setHasPlot } = useProfile(); const [step, setStep] = useState(1); const [selectedState, setSelectedState] = useState(''); - const [selectedGardenType, setSelectedGardenType] = useState(''); - const [selectedPlot, setSelectedPlot] = useState(false); + const [selectedUserType, setSelectedUserType] = useState< + UserTypeEnum | undefined + >(undefined); + const [selectedPlot, setSelectedPlot] = useState( + undefined, + ); const handleNext = () => { setStep(step + 1); @@ -115,24 +139,25 @@ const OnboardingFlow = () => { const handleBack = () => { setStep(step - 1); }; - const handleSubmit = async () => { - const profile: Profile = { - user_id: '2abd7296-374a-42d1-bb4f-b813da1615ae', - state: selectedState, - user_type: selectedGardenType, - has_plot: selectedPlot, - }; try { - upsertProfile(profile); + const profileToUpload: Profile = { + user_id: placeholderUserId, + us_state: selectedState, + user_type: selectedUserType!, + }; + + await setProfile(profileToUpload); + await setHasPlot(selectedPlot!); // Update has_plot } 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 + }; + + const disableNext = () => { + if (step === 1) return !selectedState; + if (step === 2) return !selectedUserType; + if (step === 3) return !(typeof selectedPlot === 'undefined'); }; return ( @@ -144,9 +169,9 @@ const OnboardingFlow = () => { /> )} {step === 2 && ( - )} {step === 3 && ( @@ -159,7 +184,7 @@ const OnboardingFlow = () => {
{step > 1 && } {step < 3 && ( - )} @@ -167,6 +192,4 @@ const OnboardingFlow = () => {
); -}; - -export default OnboardingFlow; +} diff --git a/app/page.tsx b/app/page.tsx index d88a5b5..1e948e7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,11 +1,10 @@ -import BPLogo from '@/assets/images/bp-logo.png'; -import { Container, Image } from './page.style'; +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/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, + }; +}; diff --git a/types/schema.d.ts b/types/schema.d.ts index ec92e59..1882b95 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; + us_state: string; + user_type: UserTypeEnum; } export interface Plant { diff --git a/utils/ProfileProvider.tsx b/utils/ProfileProvider.tsx new file mode 100644 index 0000000..13a63ff --- /dev/null +++ b/utils/ProfileProvider.tsx @@ -0,0 +1,89 @@ +'use client'; + +import React, { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { + fetchProfileById, + upsertProfile, +} from '@/api/supabase/queries/profiles'; +import { Profile } from '@/types/schema'; + +// Define a placeholder user ID for development purposes +const placeholderUserId = '0802d796-ace8-480d-851b-d16293c74a21'; + +export interface ProfileContextType { + profileData: Profile | null; + profileReady: boolean; + hasPlot: boolean | null; + setProfile: (completeProfile: Profile) => Promise; // Now expects full Profile + loadProfile: () => Promise; + setHasPlot: (plotValue: boolean | null) => void; +} + +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; +}; + +interface ProfileProviderProps { + children: ReactNode; +} +export default function ProfileProvider({ children }: ProfileProviderProps) { + const [profileData, setProfileData] = useState(null); + const [profileReady, setProfileReady] = useState(false); + const [hasPlot, setHasPlot] = useState(null); + + const loadProfile = useCallback(async () => { + setProfileReady(false); + + const fetchedProfile = await fetchProfileById(placeholderUserId); + + setProfileData(fetchedProfile); + setProfileReady(true); + }, []); + + useEffect(() => { + loadProfile(); + }, [loadProfile]); + + 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'); + } + }, []); + + const providerValue = useMemo( + () => ({ + profileData, + profileReady, + hasPlot: hasPlot, + setProfile, + loadProfile, + setHasPlot, + }), + [profileData, profileReady, hasPlot, setProfile, loadProfile, setHasPlot], + ); + + return ( + + {children} + + ); +}