diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index 5429ff1..78d387c 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -1,4 +1,4 @@ -import { HabitsProvider, OccurrencesProvider } from '@context'; +import { OccurrencesProvider } from '@context'; import { supabaseClient } from '@helpers'; import { NextUIProvider } from '@nextui-org/react'; import { SessionContextProvider } from '@supabase/auth-helpers-react'; @@ -15,9 +15,7 @@ const LowerProviders = ({ children }: ProviderProps) => { return ( - - {children} - + {children} ); }; diff --git a/src/components/calendar/CalendarHeader.test.tsx b/src/components/calendar/CalendarHeader.test.tsx index 074c4b0..43d181b 100644 --- a/src/components/calendar/CalendarHeader.test.tsx +++ b/src/components/calendar/CalendarHeader.test.tsx @@ -1,4 +1,4 @@ -import { HabitsProvider, OccurrencesProvider } from '@context'; +import { OccurrencesProvider } from '@context'; import { render } from '@testing-library/react'; import React from 'react'; @@ -25,50 +25,42 @@ describe(CalendarHeader.name, () => { it.skip('should render month and year', () => { const { getByText } = render( - - - - - + + + ); expect(getByText('January 2022')).toBeInTheDocument(); }); it.skip('should disable previous button', () => { const { getByRole } = render( - - - - - + + + ); expect(getByRole('navigate-back')).toBeDisabled(); }); it.skip('should disable next button', () => { const { getByRole } = render( - - - - - + + + ); expect(getByRole('navigate-forward')).toBeDisabled(); }); it.skip('should call onNavigateBack', () => { const { getByRole } = render( - - - - - + + + ); getByRole('navigate-back').click(); expect(props.onNavigateBack).toHaveBeenCalled(); @@ -76,11 +68,9 @@ describe(CalendarHeader.name, () => { it.skip('should call onNavigateForward', () => { const { getByRole } = render( - - - - - + + + ); getByRole('navigate-forward').click(); expect(props.onNavigateForward).toHaveBeenCalled(); diff --git a/src/components/calendar/CalendarHeader.tsx b/src/components/calendar/CalendarHeader.tsx index fe0d971..4cb4e51 100644 --- a/src/components/calendar/CalendarHeader.tsx +++ b/src/components/calendar/CalendarHeader.tsx @@ -1,8 +1,8 @@ -import { useHabits, useOccurrences } from '@context'; +import { useOccurrences } from '@context'; import { useScreenSize } from '@hooks'; import { Select, SelectItem, Button } from '@nextui-org/react'; import { ArrowFatLeft, ArrowFatRight } from '@phosphor-icons/react'; -import { useTraitsStore } from '@stores'; +import { useTraitsStore, useHabitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import React from 'react'; @@ -51,7 +51,7 @@ const CalendarHeader = ({ onNavigateToYear, onResetFocusedDate, }: CalendarHeaderProps) => { - const { habits } = useHabits(); + const { habits } = useHabitsStore(); const { traits } = useTraitsStore(); const { filteredBy, filterBy } = useOccurrences(); const user = useUser(); diff --git a/src/components/calendar/DayHabitModalDialog.test.tsx b/src/components/calendar/DayHabitModalDialog.test.tsx index 6d8140a..073af40 100644 --- a/src/components/calendar/DayHabitModalDialog.test.tsx +++ b/src/components/calendar/DayHabitModalDialog.test.tsx @@ -1,4 +1,5 @@ -import { useHabits, useOccurrences } from '@context'; +import { useOccurrences } from '@context'; +import { useHabitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { makeTestHabit } from '@tests'; @@ -9,10 +10,13 @@ import DayHabitModalDialog from './DayHabitModalDialog'; jest.mock('@context', () => ({ useOccurrences: jest.fn(), - useHabits: jest.fn(), useSnackbar: jest.fn().mockReturnValue({ showSnackbar: jest.fn() }), })); +jest.mock('@stores', () => ({ + useHabitsStore: jest.fn(), +})); + jest.mock('@supabase/auth-helpers-react', () => ({ useUser: jest.fn(), })); @@ -36,7 +40,7 @@ describe(DayHabitModalDialog.name, () => { }); it('should render', () => { - (useHabits as jest.Mock).mockReturnValue({ habits: [] }); + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [] }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); (format as jest.Mock).mockReturnValue('2021-01-01'); (useOccurrences as jest.Mock).mockReturnValue({ @@ -48,7 +52,7 @@ describe(DayHabitModalDialog.name, () => { }); it('should not render if date is null', () => { - (useHabits as jest.Mock).mockReturnValue({ habits: [] }); + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [] }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); (format as jest.Mock).mockReturnValue('2021-01-01'); (useOccurrences as jest.Mock).mockReturnValue({ @@ -62,7 +66,7 @@ describe(DayHabitModalDialog.name, () => { }); it('should not render if open is false', () => { - (useHabits as jest.Mock).mockReturnValue({ habits: [] }); + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [] }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); (format as jest.Mock).mockReturnValue('2021-01-01'); (useOccurrences as jest.Mock).mockReturnValue({ @@ -76,7 +80,7 @@ describe(DayHabitModalDialog.name, () => { }); it('should not render if date is null', () => { - (useHabits as jest.Mock).mockReturnValue({ habits: [] }); + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [] }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); (format as jest.Mock).mockReturnValue('2021-01-01'); (useOccurrences as jest.Mock).mockReturnValue({ @@ -90,7 +94,7 @@ describe(DayHabitModalDialog.name, () => { }); it('if no habits are available, should show a message', () => { - (useHabits as jest.Mock).mockReturnValue({ habits: [] }); + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [] }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); (format as jest.Mock).mockReturnValue('2021-01-01'); (useOccurrences as jest.Mock).mockReturnValue({ @@ -102,7 +106,7 @@ describe(DayHabitModalDialog.name, () => { }); it('should render habit options', () => { - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [makeTestHabit()], }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); @@ -116,7 +120,7 @@ describe(DayHabitModalDialog.name, () => { }); it.skip('should select habit', async () => { - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [makeTestHabit({ id: 42 })], }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); @@ -137,7 +141,7 @@ describe(DayHabitModalDialog.name, () => { }); it('on close, should call onClose', () => { - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [makeTestHabit()], }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); @@ -152,7 +156,7 @@ describe(DayHabitModalDialog.name, () => { }); it.skip('on close, should unselect habit', () => { - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [makeTestHabit()], }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); @@ -174,7 +178,7 @@ describe(DayHabitModalDialog.name, () => { }); it.skip('on submit, should call addOccurrence with proper arguments', () => { - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ habits: [makeTestHabit()], }); (useUser as jest.Mock).mockReturnValue({ id: '1' }); diff --git a/src/components/calendar/DayHabitModalDialog.tsx b/src/components/calendar/DayHabitModalDialog.tsx index 53de40e..21326c0 100644 --- a/src/components/calendar/DayHabitModalDialog.tsx +++ b/src/components/calendar/DayHabitModalDialog.tsx @@ -1,4 +1,4 @@ -import { useOccurrences, useHabits } from '@context'; +import { useOccurrences } from '@context'; import { Button, Modal, @@ -9,6 +9,7 @@ import { Select, SelectItem, } from '@nextui-org/react'; +import { useHabitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import { format } from 'date-fns'; import React, { type MouseEventHandler } from 'react'; @@ -24,7 +25,7 @@ const DayHabitModalDialog = ({ onClose, date, }: DayHabitModalDialogProps) => { - const { habits } = useHabits(); + const { habits } = useHabitsStore(); const user = useUser(); const { addOccurrence, addingOccurrence } = useOccurrences(); const [selectedHabitIds, setSelectedHabitIds] = React.useState([]); diff --git a/src/components/habit/add-habit/AddHabitDialogButton.test.tsx b/src/components/habit/add-habit/AddHabitDialogButton.test.tsx index bac08ae..e2d3350 100644 --- a/src/components/habit/add-habit/AddHabitDialogButton.test.tsx +++ b/src/components/habit/add-habit/AddHabitDialogButton.test.tsx @@ -1,6 +1,5 @@ -import { useHabits } from '@context'; import { StorageBuckets, uploadFile } from '@services'; -// import { useSnackbarsStore } from '@stores'; +import { useSnackbarsStore, useHabitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import { act, fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; @@ -8,15 +7,13 @@ import React from 'react'; import AddHabitDialogButton from './AddHabitDialogButton'; jest.mock('@context', () => ({ - useHabits: jest.fn().mockReturnValue({ updateHabit: jest.fn() }), useSnackbar: jest.fn().mockReturnValue({ showSnackbar: jest.fn() }), - useTraits: jest.fn().mockReturnValue({ - traits: [{ id: 1, slug: 'trait-slug', name: 'Trait' }], - }), })); jest.mock('@stores', () => ({ useSnackbarsStore: jest.fn(), + useHabitsStore: jest.fn(), + useTraitsStore: jest.fn(), })); jest.mock('@services', () => ({ @@ -33,7 +30,7 @@ jest.mock('@supabase/auth-helpers-react', () => ({ describe(AddHabitDialogButton.name, () => { it.skip('should handle data enter and dialog close', () => { - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ updateHabit: jest.fn(), fetchingHabits: false, }); @@ -78,7 +75,7 @@ describe(AddHabitDialogButton.name, () => { }); it.skip('should not set habit icon if empty file uploaded', () => { - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ updateHabit: jest.fn(), fetchingHabits: false, }); @@ -98,7 +95,9 @@ describe(AddHabitDialogButton.name, () => { it.skip('should call addHabit on form submit', () => { const mockAddHabit = jest.fn().mockReturnValue(Promise.resolve({ id: 1 })); - (useHabits as jest.Mock).mockReturnValue({ addHabit: mockAddHabit }); + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ + addHabit: mockAddHabit, + }); (useUser as jest.Mock).mockReturnValue({ id: '4c6b7c3b-ec2f-45fb-8c3a-df16f7a4b3aa', }); @@ -120,20 +119,20 @@ describe(AddHabitDialogButton.name, () => { .fn() .mockReturnValue(Promise.resolve({ id: 1234 })); const mockUpdateHabit = jest.fn().mockReturnValue(Promise.resolve({})); - // const mockShowSnackbar = jest.fn(); + const mockShowSnackbar = jest.fn(); (uploadFile as jest.Mock).mockReturnValue( Promise.resolve({ data: { path: 'icon-path' } }) ); - (useHabits as jest.Mock).mockReturnValue({ + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ addHabit: mockAddHabit, updateHabit: mockUpdateHabit, }); (useUser as jest.Mock).mockReturnValue({ id: 'uuid-42', }); - // (useSnackbarsStore as jest.Mock).mockReturnValue({ - // showSnackbar: mockShowSnackbar, - // }); + (useSnackbarsStore as unknown as jest.Mock).mockReturnValue({ + showSnackbar: mockShowSnackbar, + }); const { getByRole, getByTestId, getByLabelText } = render( diff --git a/src/components/habit/add-habit/AddHabitDialogButton.tsx b/src/components/habit/add-habit/AddHabitDialogButton.tsx index 50e3512..85e642c 100644 --- a/src/components/habit/add-habit/AddHabitDialogButton.tsx +++ b/src/components/habit/add-habit/AddHabitDialogButton.tsx @@ -1,5 +1,4 @@ import { AddCustomTraitModal, VisuallyHiddenInput } from '@components'; -import { useHabits } from '@context'; import { useTextField, useFileField } from '@hooks'; import { Button, @@ -14,14 +13,14 @@ import { Textarea, } from '@nextui-org/react'; import { CloudArrowUp, Plus } from '@phosphor-icons/react'; -import { useTraitsStore } from '@stores'; +import { useHabitsStore, useTraitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import React from 'react'; const AddHabitDialogButton = () => { const user = useUser(); const { traits } = useTraitsStore(); - const { fetchingHabits, addingHabit, addHabit } = useHabits(); + const { fetchingHabits, addingHabit, addHabit } = useHabitsStore(); const [open, setOpen] = React.useState(false); const [name, handleNameChange, clearName] = useTextField(); const [description, handleDescriptionChange, clearDescription] = diff --git a/src/components/habit/edit-habit/EditHabitDialog.test.tsx b/src/components/habit/edit-habit/EditHabitDialog.test.tsx index a45deb3..6077d01 100644 --- a/src/components/habit/edit-habit/EditHabitDialog.test.tsx +++ b/src/components/habit/edit-habit/EditHabitDialog.test.tsx @@ -1,7 +1,6 @@ -import { useHabits } from '@context'; -// import { useTraitsStore } from '@stores'; +import { useHabitsStore, useTraitsStore } from '@stores'; import { act, fireEvent, render, waitFor } from '@testing-library/react'; -// import { makeTestTrait } from '@tests'; +import { makeTestTrait } from '@tests'; import React from 'react'; import EditHabitDialog, { type EditHabitDialogProps } from './EditHabitDialog'; @@ -18,8 +17,12 @@ jest.mock('@hooks', () => ({ useFileField: jest.fn().mockReturnValue([null, jest.fn(), jest.fn()]), })); -jest.mock('@context', () => ({ - useHabits: jest.fn().mockReturnValue({ updateHabit: jest.fn() }), +jest.mock('@stores', () => ({ + useHabitsStore: jest.fn().mockReturnValue({ + updateHabit: jest.fn(), + habitIdBeingUpdated: null, + }), + useTraitsStore: jest.fn().mockReturnValue({ traits: [] }), })); jest.mock('@supabase/auth-helpers-react', () => ({ @@ -86,10 +89,12 @@ describe(EditHabitDialog.name, () => { it.skip('should call updateHabit when submitted', async () => { const mockUpdateHabit = jest.fn(); - (useHabits as jest.Mock).mockReturnValue({ updateHabit: mockUpdateHabit }); - // (useTraitsStore as jest.Mock).mockReturnValue({ - // traits: [makeTestTrait()], - // }); + (useHabitsStore as unknown as jest.Mock).mockReturnValue({ + updateHabit: mockUpdateHabit, + }); + (useTraitsStore as unknown as jest.Mock).mockReturnValue({ + traits: [makeTestTrait()], + }); const { getByRole, getByLabelText } = render( ); diff --git a/src/components/habit/edit-habit/EditHabitDialog.tsx b/src/components/habit/edit-habit/EditHabitDialog.tsx index 4853975..179c17f 100644 --- a/src/components/habit/edit-habit/EditHabitDialog.tsx +++ b/src/components/habit/edit-habit/EditHabitDialog.tsx @@ -1,4 +1,3 @@ -import { useHabits } from '@context'; import { useTextField } from '@hooks'; import type { Habit } from '@models'; import { @@ -13,7 +12,7 @@ import { SelectItem, Textarea, } from '@nextui-org/react'; -import { useTraitsStore } from '@stores'; +import { useHabitsStore, useTraitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import { toEventLike } from '@utils'; import React from 'react'; @@ -33,7 +32,7 @@ const EditHabitDialog = ({ const [name, handleNameChange] = useTextField(); const [description, handleDescriptionChange] = useTextField(); const [traitId, setTraitId] = React.useState(''); - const { updateHabit, habitIdBeingUpdated } = useHabits(); + const { updateHabit, habitIdBeingUpdated } = useHabitsStore(); const { traits } = useTraitsStore(); const user = useUser(); diff --git a/src/components/habit/habits-page/HabitIconCell.tsx b/src/components/habit/habits-page/HabitIconCell.tsx index 01d55d7..b50ed9c 100644 --- a/src/components/habit/habits-page/HabitIconCell.tsx +++ b/src/components/habit/habits-page/HabitIconCell.tsx @@ -1,7 +1,7 @@ import { VisuallyHiddenInput } from '@components'; -import { useHabits } from '@context'; import { type Habit } from '@models'; import { Button, Tooltip } from '@nextui-org/react'; +import { useHabitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import { getHabitIconUrl } from '@utils'; import React from 'react'; @@ -11,7 +11,7 @@ type HabitIconCellProps = { }; const HabitIconCell = ({ habit }: HabitIconCellProps) => { - const { updateHabit } = useHabits(); + const { updateHabit } = useHabitsStore(); const user = useUser(); const iconUrl = getHabitIconUrl(habit.iconPath); diff --git a/src/components/habit/habits-page/HabitsPage.test.tsx b/src/components/habit/habits-page/HabitsPage.test.tsx index 3471ae2..cd588ef 100644 --- a/src/components/habit/habits-page/HabitsPage.test.tsx +++ b/src/components/habit/habits-page/HabitsPage.test.tsx @@ -1,4 +1,4 @@ -import { HabitsProvider, useHabits } from '@context'; +import { useHabitsStore } from '@stores'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { makeTestHabit } from '@tests'; import React from 'react'; @@ -6,12 +6,6 @@ import React from 'react'; import HabitsPage from './HabitsPage'; jest.mock('@context', () => ({ - HabitsProvider: jest.fn(({ children }) => children), - useTraits: jest.fn().mockReturnValue({ - traits: [], - }), - useHabits: jest.fn(), - useSnackbar: jest.fn().mockReturnValue({}), useOccurrences: jest.fn().mockReturnValue({ removeOccurrencesByHabitId: jest.fn(), allOccurrences: [], @@ -28,6 +22,13 @@ jest.mock('@services', () => ({ getHabitTotalEntries: jest.fn().mockResolvedValue(0), })); +jest.mock('@stores', () => ({ + useHabitsStore: jest.fn(), + useTraitsStore: jest.fn().mockReturnValue({ + traits: [], + }), +})); + jest.mock('@hooks', () => ({ ThemeMode: { LIGHT: 'light', @@ -43,7 +44,7 @@ jest.mock('@hooks', () => ({ describe(HabitsPage.name, () => { it('should display habits', async () => { - (useHabits as jest.Mock).mockImplementation(() => ({ + (useHabitsStore as unknown as jest.Mock).mockImplementation(() => ({ habits: [ makeTestHabit({ name: 'Habit name #1', @@ -55,11 +56,7 @@ describe(HabitsPage.name, () => { }), ], })); - const { getByText } = render( - - - - ); + const { getByText } = render(); await waitFor(() => { expect(getByText('Your habits')); expect(getByText('Habit name #1')); @@ -70,7 +67,7 @@ describe(HabitsPage.name, () => { }); it('should open edit dialog on edit icon button click', async () => { - (useHabits as jest.Mock).mockImplementation(() => ({ + (useHabitsStore as unknown as jest.Mock).mockImplementation(() => ({ habits: [ makeTestHabit({ id: 42, @@ -83,22 +80,14 @@ describe(HabitsPage.name, () => { }), ], })); - const { queryByRole, getByRole, getByTestId } = render( - - - - ); + const { queryByRole, getByRole, getByTestId } = render(); expect(queryByRole('submit-edited-habit-button')).toBeNull(); fireEvent.click(getByTestId('edit-habit-id-42-button')); expect(getByRole('submit-edited-habit-button')).toBeDefined(); }); it.skip('should open confirm dialog on remove icon button click', async () => { - const { getByRole, getByTestId } = render( - - - - ); + const { getByRole, getByTestId } = render(); fireEvent.click(getByTestId('delete-habit-id-2-button')); expect(getByRole('dialog')).toBeDefined(); fireEvent.click(getByRole('confirm-dialog-cancel')); @@ -106,7 +95,7 @@ describe(HabitsPage.name, () => { it.skip('should remove habit on confirm', async () => { const mockRemoveHabit = jest.fn(); - (useHabits as jest.Mock).mockImplementation(() => ({ + (useHabitsStore as unknown as jest.Mock).mockImplementation(() => ({ habits: [ makeTestHabit({ name: 'Habit name #1', @@ -119,11 +108,7 @@ describe(HabitsPage.name, () => { ], removeHabit: mockRemoveHabit, })); - const { queryByRole, getByRole, getByTestId } = render( - - - - ); + const { queryByRole, getByRole, getByTestId } = render(); expect(queryByRole('dialog')).toBeNull(); fireEvent.click(getByTestId('delete-habit-id-2-button')); expect(getByRole('dialog')).toBeDefined(); diff --git a/src/components/habit/habits-page/HabitsPage.tsx b/src/components/habit/habits-page/HabitsPage.tsx index 5701fc4..a468986 100644 --- a/src/components/habit/habits-page/HabitsPage.tsx +++ b/src/components/habit/habits-page/HabitsPage.tsx @@ -3,7 +3,7 @@ import { ConfirmDialog, EditHabitDialog, } from '@components'; -import { useHabits, useOccurrences } from '@context'; +import { useOccurrences } from '@context'; import { useDocumentTitle } from '@hooks'; import { type Habit } from '@models'; import { @@ -18,6 +18,7 @@ import { Tooltip, } from '@nextui-org/react'; import { PencilSimple, TrashSimple } from '@phosphor-icons/react'; +import { useHabitsStore } from '@stores'; import { useUser } from '@supabase/auth-helpers-react'; import { format } from 'date-fns'; import React from 'react'; @@ -64,7 +65,7 @@ const habitColumns = [ const HabitsPage = () => { const user = useUser(); - const { habits, removeHabit, habitIdBeingDeleted } = useHabits(); + const { habits, removeHabit, habitIdBeingDeleted } = useHabitsStore(); const { removeOccurrencesByHabitId } = useOccurrences(); const [habitToEdit, setHabitToEdit] = React.useState(null); const [habitToRemove, setHabitToRemove] = React.useState(null); diff --git a/src/context/Habits/HabitsContext.ts b/src/context/Habits/HabitsContext.ts deleted file mode 100644 index 2cd6181..0000000 --- a/src/context/Habits/HabitsContext.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Habit, HabitsInsert, HabitsUpdate } from '@models'; -import React from 'react'; - -type HabitsContextType = { - habitIdBeingUpdated: number | null; - habitIdBeingDeleted: number | null; - addingHabit: boolean; - fetchingHabits: boolean; - habits: Habit[]; - addHabit: (habit: HabitsInsert, icon?: File | null) => Promise; - removeHabit: (habit: Habit) => Promise; - updateHabit: ( - habitId: number, - userId: string, - habit: HabitsUpdate, - icon?: File | null - ) => Promise; -}; - -export const HabitsContext = React.createContext( - null -); - -export const useHabits = () => { - const context = React.useContext(HabitsContext); - - if (!context) { - throw new Error('useHabits must be used within a HabitsProvider'); - } - - return context; -}; diff --git a/src/context/Habits/HabitsProvider.tsx b/src/context/Habits/HabitsProvider.tsx deleted file mode 100644 index 9fbd2c7..0000000 --- a/src/context/Habits/HabitsProvider.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { HabitsContext } from '@context'; -import { useDataFetch } from '@hooks'; -import type { Habit, HabitsInsert, HabitsUpdate } from '@models'; -import { - createHabit, - deleteFile, - destroyHabit, - listHabits, - patchHabit, - StorageBuckets, - uploadFile, -} from '@services'; -import { useSnackbarsStore } from '@stores'; -import { makeTestHabit } from '@tests'; -import { getErrorMessage } from '@utils'; -import React, { type ReactNode } from 'react'; - -const HabitsProvider = ({ children }: { children: ReactNode }) => { - const { showSnackbar } = useSnackbarsStore(); - const [addingHabit, setAddingHabit] = React.useState(false); - const [fetchingHabits, setFetchingHabits] = React.useState(false); - const [habits, setHabits] = React.useState([makeTestHabit()]); - const [habitIdBeingUpdated, setHabitIdBeingUpdated] = React.useState< - number | null - >(null); - const [habitIdBeingDeleted, setHabitIdBeingDeleted] = React.useState< - number | null - >(null); - - const fetchHabits = React.useCallback(async () => { - try { - setFetchingHabits(true); - setHabits(await listHabits()); - } catch (error) { - console.error(error); - showSnackbar( - 'Something went wrong while fetching your habits. Please try reloading the page.', - { - description: `Error details: ${getErrorMessage(error)}`, - color: 'danger', - dismissible: true, - } - ); - } finally { - setFetchingHabits(false); - } - }, [showSnackbar]); - - const clearHabits = React.useCallback(() => { - setHabits([]); - }, []); - - useDataFetch({ - clear: clearHabits, - load: fetchHabits, - }); - - const uploadHabitIcon = async (userId: string, icon?: File | null) => { - let iconPath = ''; - - if (icon) { - iconPath = `${userId}/${Date.now()}-${icon.name}`; - await uploadFile(StorageBuckets.HABIT_ICONS, iconPath, icon); - } - - return iconPath; - }; - - const addHabit = React.useCallback( - async (habit: HabitsInsert, icon?: File | null) => { - try { - setAddingHabit(true); - - const iconPath = await uploadHabitIcon(habit.userId, icon); - - const newHabit = await createHabit({ ...habit, iconPath }); - - setHabits((prevHabits) => [...prevHabits, newHabit]); - - showSnackbar('Your habit has been added!', { - color: 'success', - dismissible: true, - dismissText: 'Done', - }); - } catch (error) { - showSnackbar( - 'Something went wrong while adding your habit. Please try again.', - { - description: `Error details: ${getErrorMessage(error)}`, - color: 'danger', - dismissible: true, - } - ); - - console.error(error); - } finally { - setAddingHabit(false); - } - }, - [showSnackbar] - ); - - const updateHabit = React.useCallback( - async ( - id: number, - userId: string, - habit: HabitsUpdate, - icon?: File | null - ) => { - try { - setHabitIdBeingUpdated(id); - - const iconPath = await uploadHabitIcon(userId, icon); - - const updatedHabit = await patchHabit(id, { ...habit, iconPath }); - - setHabits((prevHabits) => { - const habitIndex = prevHabits.findIndex((h) => h.id === id); - const nextHabits = [...prevHabits]; - nextHabits[habitIndex] = updatedHabit; - return nextHabits; - }); - - showSnackbar('Your habit has been updated!', { - color: 'success', - dismissible: true, - }); - } catch (error) { - showSnackbar( - 'Something went wrong while updating your habit. Please try again.', - { - description: `Error details: ${getErrorMessage(error)}`, - color: 'danger', - dismissible: true, - } - ); - - console.error(error); - } finally { - setHabitIdBeingUpdated(null); - } - }, - [showSnackbar] - ); - - const removeHabit = React.useCallback( - async ({ id, iconPath }: Habit) => { - try { - setHabitIdBeingDeleted(id); - - await destroyHabit(id); - - if (iconPath) { - await deleteFile(StorageBuckets.HABIT_ICONS, iconPath); - } - - setHabits((prevHabits) => { - return prevHabits.filter((habit) => habit.id !== id); - }); - - showSnackbar('Your habit has been deleted.', { - dismissible: true, - }); - } catch (error) { - showSnackbar( - 'Something went wrong while deleting your habit. Please try again.', - { - description: `Error details: ${getErrorMessage(error)}`, - color: 'danger', - dismissible: true, - } - ); - - console.error(error); - } finally { - setHabitIdBeingDeleted(null); - } - }, - [showSnackbar] - ); - - const value = React.useMemo(() => { - return { - habitIdBeingUpdated, - habitIdBeingDeleted, - addingHabit, - fetchingHabits, - habits, - addHabit, - removeHabit, - updateHabit, - }; - }, [ - habitIdBeingUpdated, - habitIdBeingDeleted, - addingHabit, - fetchingHabits, - habits, - addHabit, - removeHabit, - updateHabit, - ]); - - return ( - {children} - ); -}; - -export default React.memo(HabitsProvider); diff --git a/src/context/Habits/index.ts b/src/context/Habits/index.ts deleted file mode 100644 index a82d71b..0000000 --- a/src/context/Habits/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './HabitsContext'; -export * from './HabitsProvider'; -export { default as HabitsProvider } from './HabitsProvider'; diff --git a/src/context/Occurrences/OccurrencesProvider.tsx b/src/context/Occurrences/OccurrencesProvider.tsx index 00e9778..f003cd9 100644 --- a/src/context/Occurrences/OccurrencesProvider.tsx +++ b/src/context/Occurrences/OccurrencesProvider.tsx @@ -1,4 +1,4 @@ -import { OccurrencesContext, useHabits } from '@context'; +import { OccurrencesContext } from '@context'; import { cacheOccurrence, occurrencesCache, uncacheOccurrence } from '@helpers'; import { useDataFetch } from '@hooks'; import type { @@ -11,7 +11,7 @@ import { destroyOccurrence, listOccurrences, } from '@services'; -import { useSnackbarsStore, useTraitsStore } from '@stores'; +import { useHabitsStore, useSnackbarsStore, useTraitsStore } from '@stores'; import { getErrorMessage } from '@utils'; import React, { type ReactNode } from 'react'; @@ -26,7 +26,7 @@ export type OccurrenceFilters = { const OccurrencesProvider = ({ children }: Props) => { const { showSnackbar } = useSnackbarsStore(); - const { habits } = useHabits(); + const { habits } = useHabitsStore(); const { traits } = useTraitsStore(); const [addingOccurrence, setAddingOccurrence] = React.useState(false); @@ -69,7 +69,7 @@ const OccurrencesProvider = ({ children }: Props) => { }, [range, showSnackbar]); const clearOccurrences = React.useCallback(() => { - setOccurrences([]); + setAllOccurrences([]); occurrencesCache.clear(); }, []); @@ -197,7 +197,7 @@ const OccurrencesProvider = ({ children }: Props) => { ); const removeOccurrencesByHabitId = (habitId: number) => { - setOccurrences((prevOccurrences) => { + setAllOccurrences((prevOccurrences) => { return prevOccurrences.filter((occurrence) => { return occurrence.habitId !== habitId; }); diff --git a/src/context/index.ts b/src/context/index.ts index e080738..40ff0a0 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -1,3 +1 @@ -export * from './Habits'; - export * from './Occurrences'; diff --git a/src/index.tsx b/src/index.tsx index e5bd876..327f931 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,7 +9,7 @@ const root = ReactDOM.createRoot( ); root.render( - // - - // + + + ); diff --git a/src/stores/habits.store.ts b/src/stores/habits.store.ts new file mode 100644 index 0000000..6368dce --- /dev/null +++ b/src/stores/habits.store.ts @@ -0,0 +1,171 @@ +import type { Habit, HabitsInsert, HabitsUpdate } from '@models'; +import { + createHabit, + deleteFile, + destroyHabit, + listHabits, + patchHabit, + StorageBuckets, + uploadFile, +} from '@services'; +import { makeTestHabit } from '@tests'; +import { getErrorMessage } from '@utils'; +import { create } from 'zustand'; + +import { useSnackbarsStore } from './index'; + +const { showSnackbar } = useSnackbarsStore.getState(); + +type HabitsState = { + habits: Habit[]; + addingHabit: boolean; + fetchingHabits: boolean; + habitIdBeingUpdated: number | null; + habitIdBeingDeleted: number | null; + fetchHabits: () => Promise; + addHabit: (habit: HabitsInsert, icon?: File | null) => Promise; + updateHabit: ( + id: number, + userId: string, + habit: HabitsUpdate, + icon?: File | null + ) => Promise; + removeHabit: (habit: Habit) => Promise; +}; + +const useHabitsStore = create((set) => ({ + habits: [makeTestHabit()], + addingHabit: false, + fetchingHabits: false, + habitIdBeingUpdated: null, + habitIdBeingDeleted: null, + + fetchHabits: async () => { + set({ fetchingHabits: true }); + + try { + const habits = await listHabits(); + set({ habits }); + } catch (error) { + console.error(error); + + showSnackbar( + 'Something went wrong while fetching your habits. Please try reloading the page.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + } finally { + set({ fetchingHabits: false }); + } + }, + + addHabit: async (habit: HabitsInsert, icon?: File | null) => { + set({ addingHabit: true }); + + try { + const iconPath = await uploadHabitIcon(habit.userId, icon); + const newHabit = await createHabit({ ...habit, iconPath }); + set((state) => ({ habits: [...state.habits, newHabit] })); + + showSnackbar('Your habit has been added!', { + color: 'success', + dismissible: true, + dismissText: 'Done', + }); + } catch (error) { + console.error(error); + + showSnackbar( + 'Something went wrong while adding your habit. Please try again.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + } finally { + set({ addingHabit: false }); + } + }, + + updateHabit: async ( + id: number, + userId: string, + habit: HabitsUpdate, + icon?: File | null + ) => { + set({ habitIdBeingUpdated: id }); + + try { + const iconPath = await uploadHabitIcon(userId, icon); + const updatedHabit = await patchHabit(id, { ...habit, iconPath }); + set((state) => ({ + habits: state.habits.map((h) => (h.id === id ? updatedHabit : h)), + })); + showSnackbar('Your habit has been updated!', { + color: 'success', + dismissible: true, + }); + } catch (error) { + console.error(error); + + showSnackbar( + 'Something went wrong while updating your habit. Please try again.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + } finally { + set({ habitIdBeingUpdated: null }); + } + }, + + removeHabit: async ({ id, iconPath }: Habit) => { + set({ habitIdBeingDeleted: id }); + + try { + await destroyHabit(id); + + if (iconPath) { + await deleteFile(StorageBuckets.HABIT_ICONS, iconPath); + } + + set((state) => ({ + habits: state.habits.filter((habit) => habit.id !== id), + })); + + showSnackbar('Your habit has been deleted.', { + dismissible: true, + }); + } catch (error) { + console.error(error); + + showSnackbar( + 'Something went wrong while deleting your habit. Please try again.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + } finally { + set({ habitIdBeingDeleted: null }); + } + }, +})); + +const uploadHabitIcon = async (userId: string, icon?: File | null) => { + let iconPath = ''; + if (icon) { + iconPath = `${userId}/${Date.now()}-${icon.name}`; + await uploadFile(StorageBuckets.HABIT_ICONS, iconPath, icon); + } + return iconPath; +}; + +export default useHabitsStore; diff --git a/src/stores/index.ts b/src/stores/index.ts index 4683d72..3f85349 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,2 +1,3 @@ export { default as useSnackbarsStore } from './snackbars.store'; export { default as useTraitsStore } from './traits.store'; +export { default as useHabitsStore } from './habits.store'; diff --git a/src/stores/traits.store.ts b/src/stores/traits.store.ts index fb70da2..53627f1 100644 --- a/src/stores/traits.store.ts +++ b/src/stores/traits.store.ts @@ -19,12 +19,13 @@ const testTraits = [ makeTestTrait({ name: 'Test Bad Trait', color: '#F6F6F6' }), ]; +const { showSnackbar } = useSnackbarsStore.getState(); + const useTraitsStore = create((set) => ({ traits: testTraits, fetchingTraits: false, addingTrait: false, fetchTraits: async () => { - const { showSnackbar } = useSnackbarsStore.getState(); try { set({ fetchingTraits: true }); const traits = await listTraits(); @@ -44,7 +45,6 @@ const useTraitsStore = create((set) => ({ } }, addTrait: async (trait: TraitsInsert) => { - const { showSnackbar } = useSnackbarsStore.getState(); try { set({ addingTrait: true }); const newTrait = await createTrait(trait);