diff --git a/src/contexts/AuthContext.ts b/src/contexts/AuthContext.ts new file mode 100644 index 00000000..0ac1f12a --- /dev/null +++ b/src/contexts/AuthContext.ts @@ -0,0 +1,23 @@ +// src/contexts/AuthContext.ts +import { createContext } from 'react'; +import type { User, UserProfile } from '../types'; + +/** + * Defines the possible authentication states for a user session. + * - `Determining...`: The initial state while checking for a token. + * - `SIGNED_OUT`: No user is active. + * - `AUTHENTICATED`: The user has successfully logged in. + */ +export type AuthStatus = 'Determining...' | 'SIGNED_OUT' | 'AUTHENTICATED'; + +export interface AuthContextType { + user: User | null; + profile: UserProfile | null; + authStatus: AuthStatus; + isLoading: boolean; + login: (user: User, token: string) => Promise; + logout: () => void; + updateProfile: (updatedProfileData: Partial) => void; +} + +export const AuthContext = createContext(undefined); \ No newline at end of file diff --git a/src/contexts/FlyersContext.ts b/src/contexts/FlyersContext.ts new file mode 100644 index 00000000..60976967 --- /dev/null +++ b/src/contexts/FlyersContext.ts @@ -0,0 +1,15 @@ +// src/contexts/FlyersContext.ts +import { createContext } from 'react'; +import type { Flyer } from '../types'; + +export interface FlyersContextType { + flyers: Flyer[]; + isLoadingFlyers: boolean; + flyersError: Error | null; + fetchNextFlyersPage: () => void; + hasNextFlyersPage: boolean; + isRefetchingFlyers: boolean; + refetchFlyers: () => void; +} + +export const FlyersContext = createContext(undefined); \ No newline at end of file diff --git a/src/contexts/MasterItemsContext.ts b/src/contexts/MasterItemsContext.ts new file mode 100644 index 00000000..4ad0ff72 --- /dev/null +++ b/src/contexts/MasterItemsContext.ts @@ -0,0 +1,11 @@ +// src/contexts/MasterItemsContext.ts +import { createContext } from 'react'; +import type { MasterGroceryItem } from '../types'; + +export interface MasterItemsContextType { + masterItems: MasterGroceryItem[]; + isLoading: boolean; + error: string | null; +} + +export const MasterItemsContext = createContext(undefined); \ No newline at end of file diff --git a/src/contexts/ModalContext.ts b/src/contexts/ModalContext.ts new file mode 100644 index 00000000..674de836 --- /dev/null +++ b/src/contexts/ModalContext.ts @@ -0,0 +1,20 @@ +// src/contexts/ModalContext.ts +import { createContext } from 'react'; + +/** + * Defines the names of all modals used in the application. + * Using a type ensures consistency and prevents typos. + */ +export type ModalType = 'profile' | 'voiceAssistant' | 'whatsNew' | 'correctionTool'; + +/** + * Defines the shape of the context that will be provided to consumers. + */ +export interface ModalContextType { + openModal: (modal: ModalType) => void; + closeModal: (modal: ModalType) => void; + isModalOpen: (modal: ModalType) => boolean; +} + +// Create the context with a default value (which should not be used directly). +export const ModalContext = createContext(undefined); \ No newline at end of file diff --git a/src/contexts/ModalContext.tsx b/src/contexts/ModalContext.tsx index 84901fa2..e69de29b 100644 --- a/src/contexts/ModalContext.tsx +++ b/src/contexts/ModalContext.tsx @@ -1,55 +0,0 @@ -// src/contexts/ModalContext.tsx -import React, { createContext, useState, useMemo, useCallback, useContext } from 'react'; - -/** - * Defines the names of all modals used in the application. - * Using a type ensures consistency and prevents typos. - */ -export type ModalType = 'profile' | 'voiceAssistant' | 'whatsNew' | 'correctionTool'; - -/** - * Defines the shape of the context that will be provided to consumers. - */ -export interface ModalContextType { - openModal: (modal: ModalType) => void; - closeModal: (modal: ModalType) => void; - isModalOpen: (modal: ModalType) => boolean; -} - -// Create the context with a default value (which should not be used directly). -export const ModalContext = createContext(undefined); - -/** - * The provider component that will wrap the application. - * It holds the state for all modals and provides functions to control them. - */ -export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [modalState, setModalState] = useState>({ - profile: false, - voiceAssistant: false, - whatsNew: false, - correctionTool: false, - }); - - const openModal = useCallback((modal: ModalType) => setModalState(prev => ({ ...prev, [modal]: true })), []); - const closeModal = useCallback((modal: ModalType) => setModalState(prev => ({ ...prev, [modal]: false })), []); - const isModalOpen = useCallback((modal: ModalType) => modalState[modal], [modalState]); - - // useMemo ensures the context value object is stable across re-renders, - // preventing unnecessary re-renders of consumer components. - const value = useMemo(() => ({ openModal, closeModal, isModalOpen }), [openModal, closeModal, isModalOpen]); - - return {children}; -}; - -/** - * The custom hook that components will use to access the modal context. - * It provides a clean and simple API for interacting with modals. - */ -export const useModal = (): ModalContextType => { - const context = useContext(ModalContext); - if (context === undefined) { - throw new Error('useModal must be used within a ModalProvider'); - } - return context; -}; \ No newline at end of file diff --git a/src/contexts/UserDataContext.ts b/src/contexts/UserDataContext.ts new file mode 100644 index 00000000..6e1500e0 --- /dev/null +++ b/src/contexts/UserDataContext.ts @@ -0,0 +1,14 @@ +// src/contexts/UserDataContext.ts +import React from 'react'; +import type { MasterGroceryItem, ShoppingList } from '../types'; + +export interface UserDataContextType { + watchedItems: MasterGroceryItem[]; + shoppingLists: ShoppingList[]; + setWatchedItems: React.Dispatch>; + setShoppingLists: React.Dispatch>; + isLoading: boolean; + error: string | null; +} + +export const UserDataContext = React.createContext(undefined); \ No newline at end of file diff --git a/src/contexts/useAuth.tsx b/src/contexts/useAuth.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/contexts/useFlyers.tsx b/src/contexts/useFlyers.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/contexts/useMasterItems.tsx b/src/contexts/useMasterItems.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/contexts/useModal.tsx b/src/contexts/useModal.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/contexts/useUserData.tsx b/src/contexts/useUserData.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/hooks/useActiveDeals.test.tsx b/src/hooks/useActiveDeals.test.tsx index 8850263b..c72017e3 100644 --- a/src/hooks/useActiveDeals.test.tsx +++ b/src/hooks/useActiveDeals.test.tsx @@ -2,17 +2,17 @@ import { renderHook, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { useActiveDeals } from './useActiveDeals'; -import * as apiClient from '../services/apiClient'; // Correct path -import { useFlyers } from '../contexts/FlyersContext'; -import { useUserData } from './useUserData'; +import * as apiClient from '../services/apiClient'; +import { useFlyers } from '../hooks/useFlyers'; +import { useUserData } from '../hooks/useUserData'; import type { Flyer, MasterGroceryItem, FlyerItem, DealItem } from '../types'; // The apiClient is globally mocked in our test setup, so we just need to cast it const mockedApiClient = vi.mocked(apiClient); // Mock the new data provider hooks -vi.mock('../contexts/FlyersContext'); -vi.mock('./useUserData'); +vi.mock('../hooks/useFlyers'); +vi.mock('../hooks/useUserData'); // Create typed mocks for easier usage const mockedUseFlyers = vi.mocked(useFlyers); diff --git a/src/hooks/useActiveDeals.tsx b/src/hooks/useActiveDeals.tsx index 647ec4b6..d7d743b6 100644 --- a/src/hooks/useActiveDeals.tsx +++ b/src/hooks/useActiveDeals.tsx @@ -1,7 +1,7 @@ // src/hooks/useActiveDeals.tsx import { useEffect, useMemo } from 'react'; -import { useFlyers } from '../contexts/FlyersContext'; -import { useUserData } from './useUserData'; +import { useFlyers } from './useFlyers'; +import { useUserData } from '../hooks/useUserData'; import { useApi } from './useApi'; import type { FlyerItem, DealItem } from '../types'; import * as apiClient from '../services/apiClient'; diff --git a/src/hooks/useAuth.test.tsx b/src/hooks/useAuth.test.tsx index 047fa249..5d65ace0 100644 --- a/src/hooks/useAuth.test.tsx +++ b/src/hooks/useAuth.test.tsx @@ -1,7 +1,8 @@ import React, { ReactNode } from 'react'; import { renderHook, waitFor, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { AuthProvider, useAuth } from '../contexts/AuthContext'; +import { useAuth } from './useAuth'; +import { AuthProvider } from '../providers/AuthProvider'; import * as apiClient from '../services/apiClient'; import type { User, UserProfile } from '../types'; diff --git a/src/hooks/useFlyers.test.tsx b/src/hooks/useFlyers.test.tsx index 8d381d0f..08501e29 100644 --- a/src/hooks/useFlyers.test.tsx +++ b/src/hooks/useFlyers.test.tsx @@ -2,7 +2,8 @@ import React, { ReactNode } from 'react'; import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { FlyersProvider, useFlyers } from '../contexts/FlyersContext'; +import { useFlyers } from './useFlyers'; +import { FlyersProvider } from '../providers/FlyersProvider'; import { useInfiniteQuery } from './useInfiniteQuery'; import type { Flyer } from '../types'; diff --git a/src/hooks/useMasterItems.test.tsx b/src/hooks/useMasterItems.test.tsx index 39b79f8c..80964e01 100644 --- a/src/hooks/useMasterItems.test.tsx +++ b/src/hooks/useMasterItems.test.tsx @@ -2,7 +2,8 @@ import React, { ReactNode } from 'react'; import { renderHook } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { MasterItemsProvider, useMasterItems } from '../contexts/MasterItemsContext'; +import { useMasterItems } from './useMasterItems'; +import { MasterItemsProvider } from '../providers/MasterItemsProvider'; import { useApiOnMount } from './useApiOnMount'; import type { MasterGroceryItem } from '../types'; diff --git a/src/hooks/useModal.test.tsx b/src/hooks/useModal.test.tsx index 64309bb1..0ad3a215 100644 --- a/src/hooks/useModal.test.tsx +++ b/src/hooks/useModal.test.tsx @@ -2,7 +2,8 @@ import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import React from 'react'; -import { ModalProvider, useModal } from '../contexts/ModalContext'; +import { useModal } from './useModal'; +import { ModalProvider } from '../providers/ModalProvider'; // Create a wrapper component that includes the ModalProvider. // This is necessary because the useModal hook depends on the context provided by ModalProvider. diff --git a/src/hooks/useShoppingLists.test.tsx b/src/hooks/useShoppingLists.test.tsx index a1e4aaed..d0e822f9 100644 --- a/src/hooks/useShoppingLists.test.tsx +++ b/src/hooks/useShoppingLists.test.tsx @@ -3,14 +3,14 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useShoppingLists } from './useShoppingLists'; import { useApi } from './useApi'; -import { useAuth } from './useAuth'; -import { useUserData } from './useUserData'; +import { useAuth } from '../hooks/useAuth'; +import { useUserData } from '../hooks/useUserData'; import type { ShoppingList, ShoppingListItem, User } from '../types'; // Mock the hooks that useShoppingLists depends on vi.mock('./useApi'); -vi.mock('./useAuth'); -vi.mock('./useUserData'); +vi.mock('../hooks/useAuth'); +vi.mock('../hooks/useUserData'); // The apiClient is globally mocked in our test setup, so we just need to cast it const mockedUseApi = vi.mocked(useApi); @@ -36,11 +36,11 @@ describe('useShoppingLists Hook', () => { // Provide a default implementation for the mocked hooks mockedUseApi - .mockReturnValueOnce({ execute: mockCreateListApi, error: null, loading: false, isRefetching: false, data: null }) - .mockReturnValueOnce({ execute: mockDeleteListApi, error: null, loading: false, isRefetching: false, data: null }) - .mockReturnValueOnce({ execute: mockAddItemApi, error: null, loading: false, isRefetching: false, data: null }) - .mockReturnValueOnce({ execute: mockUpdateItemApi, error: null, loading: false, isRefetching: false, data: null }) - .mockReturnValueOnce({ execute: mockRemoveItemApi, error: null, loading: false, isRefetching: false, data: null }); + .mockReturnValueOnce({ execute: mockCreateListApi, error: null, loading: false, isRefetching: false, data: null, reset: vi.fn() }) + .mockReturnValueOnce({ execute: mockDeleteListApi, error: null, loading: false, isRefetching: false, data: null, reset: vi.fn() }) + .mockReturnValueOnce({ execute: mockAddItemApi, error: null, loading: false, isRefetching: false, data: null, reset: vi.fn() }) + .mockReturnValueOnce({ execute: mockUpdateItemApi, error: null, loading: false, isRefetching: false, data: null, reset: vi.fn() }) + .mockReturnValueOnce({ execute: mockRemoveItemApi, error: null, loading: false, isRefetching: false, data: null, reset: vi.fn() }); mockedUseAuth.mockReturnValue({ user: mockUser, @@ -156,7 +156,8 @@ describe('useShoppingLists Hook', () => { error: new Error('API Failed'), loading: false, isRefetching: false, - data: null + data: null, + reset: vi.fn() }); const { result } = renderHook(() => useShoppingLists()); diff --git a/src/hooks/useShoppingLists.tsx b/src/hooks/useShoppingLists.tsx index bb18b1bf..91c56ca6 100644 --- a/src/hooks/useShoppingLists.tsx +++ b/src/hooks/useShoppingLists.tsx @@ -1,7 +1,7 @@ // src/hooks/useShoppingLists.tsx import { useState, useCallback, useEffect, useMemo } from 'react'; -import { useAuth } from '../contexts/AuthContext'; -import { useUserData } from './useUserData'; +import { useAuth } from '../hooks/useAuth'; +import { useUserData } from '../hooks/useUserData'; import { useApi } from './useApi'; import * as apiClient from '../services/apiClient'; import type { ShoppingList, ShoppingListItem } from '../types'; diff --git a/src/hooks/useUserData.test.tsx b/src/hooks/useUserData.test.tsx index 284a5242..adf02e79 100644 --- a/src/hooks/useUserData.test.tsx +++ b/src/hooks/useUserData.test.tsx @@ -2,13 +2,14 @@ import React, { ReactNode } from 'react'; import { renderHook, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { UserDataProvider, useUserData } from '../contexts/UserDataContext'; -import { useAuth } from '../contexts/AuthContext'; +import { useUserData } from '../hooks/useUserData'; +import { useAuth } from '../hooks/useAuth'; +import { UserDataProvider } from '../providers/UserDataProvider'; import { useApiOnMount } from './useApiOnMount'; import type { MasterGroceryItem, ShoppingList, UserProfile } from '../types'; // 1. Mock the hook's dependencies -vi.mock('../contexts/AuthContext'); +vi.mock('../hooks/useAuth'); vi.mock('./useApiOnMount'); // 2. Create typed mocks for type safety and autocompletion @@ -17,7 +18,7 @@ const mockedUseApiOnMount = vi.mocked(useApiOnMount); // 3. A simple wrapper component that renders our provider. // This is necessary because the useUserData hook needs to be a child of UserDataProvider. -const wrapper = ({ children }: { children: ReactNode }) => {children}; +const wrapper = ({ children }: { children: ReactNode }) => {children}; // No change needed here, just for context // 4. Mock data for testing const mockUser: UserProfile = { diff --git a/src/hooks/useWatchedItems.test.tsx b/src/hooks/useWatchedItems.test.tsx index 3d337ae5..04d42a93 100644 --- a/src/hooks/useWatchedItems.test.tsx +++ b/src/hooks/useWatchedItems.test.tsx @@ -2,15 +2,15 @@ import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useWatchedItems } from './useWatchedItems'; -import { useApi } from '../hooks/useApi'; -import { useAuth } from '../contexts/AuthContext'; -import { useUserData } from './useUserData'; +import { useApi } from './useApi'; +import { useAuth } from '../hooks/useAuth'; +import { useUserData } from '../hooks/useUserData'; import type { MasterGroceryItem, User } from '../types'; // Mock the hooks that useWatchedItems depends on -vi.mock('../hooks/useApi'); -vi.mock('../contexts/AuthContext'); -vi.mock('./useUserData'); +vi.mock('./useApi'); +vi.mock('../hooks/useAuth'); +vi.mock('../hooks/useUserData'); // The apiClient is globally mocked in our test setup, so we just need to cast it const mockedUseApi = vi.mocked(useApi); diff --git a/src/hooks/useWatchedItems.tsx b/src/hooks/useWatchedItems.tsx index 88c67252..c28e8329 100644 --- a/src/hooks/useWatchedItems.tsx +++ b/src/hooks/useWatchedItems.tsx @@ -1,8 +1,8 @@ // src/hooks/useWatchedItems.tsx import { useMemo, useCallback } from 'react'; -import { useAuth } from '../contexts/AuthContext'; +import { useAuth } from '../hooks/useAuth'; import { useApi } from './useApi'; -import { useUserData } from './useUserData'; +import { useUserData } from '../hooks/useUserData'; import * as apiClient from '../services/apiClient'; import type { MasterGroceryItem } from '../types'; diff --git a/src/index.tsx b/src/index.tsx index 25104c9b..840973a1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,12 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; -import { AuthProvider } from './contexts/AuthContext'; -import { FlyersProvider } from './contexts/FlyersContext'; -import { MasterItemsProvider } from './contexts/MasterItemsContext'; -import { UserDataProvider } from './contexts/UserDataContext'; import { BrowserRouter } from 'react-router-dom'; -import { ModalProvider } from './contexts/ModalContext'; +import { AppProviders } from './providers/AppProviders'; import './index.css'; const rootElement = document.getElementById('root'); @@ -19,17 +15,9 @@ const root = ReactDOM.createRoot(rootElement); root.render( - - - - - - - - - - - + + + ); diff --git a/src/providers/AppProviders.tsx b/src/providers/AppProviders.tsx new file mode 100644 index 00000000..052a0a12 --- /dev/null +++ b/src/providers/AppProviders.tsx @@ -0,0 +1,29 @@ +// src/providers/AppProviders.tsx +import React, { ReactNode } from 'react'; +import { AuthProvider } from './AuthProvider'; +import { FlyersProvider } from './FlyersProvider'; +import { MasterItemsProvider } from './MasterItemsProvider'; +import { ModalProvider } from './ModalProvider'; +import { UserDataProvider } from './UserDataProvider'; + +interface AppProvidersProps { + children: ReactNode; +} + +/** + * A single component to group all application-wide context providers. + * This cleans up index.tsx and makes the provider hierarchy clear. + */ +export const AppProviders: React.FC = ({ children }) => { + return ( + + + + + {children} + + + + + ); +}; \ No newline at end of file diff --git a/src/contexts/AuthContext.tsx b/src/providers/AuthProvider.tsx similarity index 74% rename from src/contexts/AuthContext.tsx rename to src/providers/AuthProvider.tsx index f638433e..11027dd1 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/providers/AuthProvider.tsx @@ -1,34 +1,15 @@ -// src/contexts/AuthContext.tsx -import React, { createContext, useState, useEffect, useCallback, ReactNode, useContext } from 'react'; +// src/providers/AuthProvider.tsx +import React, { useState, useEffect, useCallback, ReactNode } from 'react'; +import { AuthContext, AuthContextType } from '../contexts/AuthContext'; import type { User, UserProfile } from '../types'; import * as apiClient from '../services/apiClient'; import { useApi } from '../hooks/useApi'; import { logger } from '../services/logger.client'; -/** - * Defines the possible authentication states for a user session. - * - `Determining...`: The initial state while checking for a token. - * - `SIGNED_OUT`: No user is active. - * - `AUTHENTICATED`: The user has successfully logged in. - */ -export type AuthStatus = 'Determining...' | 'SIGNED_OUT' | 'AUTHENTICATED'; - -export interface AuthContextType { - user: User | null; - profile: UserProfile | null; - authStatus: AuthStatus; - isLoading: boolean; - login: (user: User, token: string) => Promise; - logout: () => void; - updateProfile: (updatedProfileData: Partial) => void; -} - -export const AuthContext = createContext(undefined); - export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [user, setUser] = useState(null); const [profile, setProfile] = useState(null); - const [authStatus, setAuthStatus] = useState('Determining...'); + const [authStatus, setAuthStatus] = useState('Determining...'); const [isLoading, setIsLoading] = useState(true); const { execute: checkTokenApi } = useApi( () => apiClient.getAuthenticatedUserProfile() @@ -120,17 +101,4 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const value = { user, profile, authStatus, isLoading, login, logout, updateProfile }; return {children}; -}; - -/** - * Custom hook to access the authentication context. - * This is what components will use to get auth state and methods. - * It also ensures that it's used within an AuthProvider. - */ -export const useAuth = (): AuthContextType => { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; }; \ No newline at end of file diff --git a/src/contexts/FlyersContext.tsx b/src/providers/FlyersProvider.tsx similarity index 54% rename from src/contexts/FlyersContext.tsx rename to src/providers/FlyersProvider.tsx index d7ac71a0..1ab1000b 100644 --- a/src/contexts/FlyersContext.tsx +++ b/src/providers/FlyersProvider.tsx @@ -1,21 +1,10 @@ -// src/contexts/FlyersContext.tsx -import React, { createContext, ReactNode, useContext } from 'react'; +// src/providers/FlyersProvider.tsx +import React, { ReactNode } from 'react'; +import { FlyersContext, FlyersContextType } from '../contexts/FlyersContext'; import type { Flyer } from '../types'; import * as apiClient from '../services/apiClient'; import { useInfiniteQuery } from '../hooks/useInfiniteQuery'; -export interface FlyersContextType { - flyers: Flyer[]; - isLoadingFlyers: boolean; - flyersError: Error | null; - fetchNextFlyersPage: () => void; - hasNextFlyersPage: boolean; - isRefetchingFlyers: boolean; - refetchFlyers: () => void; -} - -export const FlyersContext = createContext(undefined); - export const FlyersProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const { data: flyers, @@ -38,12 +27,4 @@ export const FlyersProvider: React.FC<{ children: ReactNode }> = ({ children }) }; return {children}; -}; - -export const useFlyers = (): FlyersContextType => { - const context = useContext(FlyersContext); - if (context === undefined) { - throw new Error('useFlyers must be used within a FlyersProvider'); - } - return context; }; \ No newline at end of file diff --git a/src/contexts/MasterItemsContext.tsx b/src/providers/MasterItemsProvider.tsx similarity index 50% rename from src/contexts/MasterItemsContext.tsx rename to src/providers/MasterItemsProvider.tsx index 7a9dfc95..6a87b6b2 100644 --- a/src/contexts/MasterItemsContext.tsx +++ b/src/providers/MasterItemsProvider.tsx @@ -1,17 +1,10 @@ -// src/contexts/MasterItemsContext.tsx -import React, { createContext, ReactNode, useMemo, useContext } from 'react'; +// src/providers/MasterItemsProvider.tsx +import React, { ReactNode, useMemo } from 'react'; +import { MasterItemsContext } from '../contexts/MasterItemsContext'; import type { MasterGroceryItem } from '../types'; import * as apiClient from '../services/apiClient'; import { useApiOnMount } from '../hooks/useApiOnMount'; -export interface MasterItemsContextType { - masterItems: MasterGroceryItem[]; - isLoading: boolean; - error: string | null; -} - -export const MasterItemsContext = createContext(undefined); - export const MasterItemsProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const { data, loading, error } = useApiOnMount( () => apiClient.fetchMasterItems() @@ -24,12 +17,4 @@ export const MasterItemsProvider: React.FC<{ children: ReactNode }> = ({ childre }), [data, loading, error]); return {children}; -}; - -export const useMasterItems = (): MasterItemsContextType => { - const context = useContext(MasterItemsContext); - if (context === undefined) { - throw new Error('useMasterItems must be used within a MasterItemsProvider'); - } - return context; }; \ No newline at end of file diff --git a/src/providers/ModalProvider.tsx b/src/providers/ModalProvider.tsx new file mode 100644 index 00000000..ffd59b3e --- /dev/null +++ b/src/providers/ModalProvider.tsx @@ -0,0 +1,26 @@ +// src/providers/ModalProvider.tsx +import React, { useState, useMemo, useCallback } from 'react'; +import { ModalContext, ModalContextType, ModalType } from '../contexts/ModalContext'; + +/** + * The provider component that will wrap the application. + * It holds the state for all modals and provides functions to control them. + */ +export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [modalState, setModalState] = useState>({ + profile: false, + voiceAssistant: false, + whatsNew: false, + correctionTool: false, + }); + + const openModal = useCallback((modal: ModalType) => setModalState(prev => ({ ...prev, [modal]: true })), []); + const closeModal = useCallback((modal: ModalType) => setModalState(prev => ({ ...prev, [modal]: false })), []); + const isModalOpen = useCallback((modal: ModalType) => modalState[modal], [modalState]); + + // useMemo ensures the context value object is stable across re-renders, + // preventing unnecessary re-renders of consumer components. + const value: ModalContextType = useMemo(() => ({ openModal, closeModal, isModalOpen }), [openModal, closeModal, isModalOpen]); + + return {children}; +}; \ No newline at end of file diff --git a/src/contexts/UserDataContext.tsx b/src/providers/UserDataProvider.tsx similarity index 71% rename from src/contexts/UserDataContext.tsx rename to src/providers/UserDataProvider.tsx index aa31e012..258358f7 100644 --- a/src/contexts/UserDataContext.tsx +++ b/src/providers/UserDataProvider.tsx @@ -1,20 +1,10 @@ -// src/contexts/UserDataContext.tsx -import React, { createContext, useState, useEffect, useMemo, ReactNode, useContext } from 'react'; +// src/providers/UserDataProvider.tsx +import React, { useState, useEffect, useMemo, ReactNode } from 'react'; +import { UserDataContext } from '../contexts/UserDataContext'; import type { MasterGroceryItem, ShoppingList } from '../types'; import * as apiClient from '../services/apiClient'; import { useApiOnMount } from '../hooks/useApiOnMount'; -import { useAuth } from './AuthContext'; - -export interface UserDataContextType { - watchedItems: MasterGroceryItem[]; - shoppingLists: ShoppingList[]; - setWatchedItems: React.Dispatch>; - setShoppingLists: React.Dispatch>; - isLoading: boolean; - error: string | null; -} - -export const UserDataContext = createContext(undefined); +import { useAuth } from '../hooks/useAuth'; export const UserDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const { user } = useAuth(); @@ -54,12 +44,4 @@ export const UserDataProvider: React.FC<{ children: ReactNode }> = ({ children } }), [watchedItems, shoppingLists, user, isLoadingWatched, isLoadingShoppingLists, watchedItemsError, shoppingListsError]); return {children}; -}; - -export const useUserData = (): UserDataContextType => { - const context = useContext(UserDataContext); - if (context === undefined) { - throw new Error('useUserData must be used within a UserDataProvider'); - } - return context; }; \ No newline at end of file