Skip to content

Commit

Permalink
fix(occurrences): fetch on range change (#117)
Browse files Browse the repository at this point in the history
* fix(occurrences): fetch on range change

* fix test
  • Loading branch information
domhhv authored Oct 25, 2024
1 parent 25f7886 commit ec53622
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 72 deletions.
2 changes: 1 addition & 1 deletion src/components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const LowerProviders = ({ children }: ProviderProps) => {
return <NextUIProvider navigate={navigate}>{children}</NextUIProvider>;
};

const PotentialSupabaseProvider = ({ children }: { children: ReactNode }) => {
const PotentialSupabaseProvider = ({ children }: ProviderProps) => {
if (!Object.keys(supabaseClient).length) {
return children;
}
Expand Down
68 changes: 36 additions & 32 deletions src/components/calendar/CalendarCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@ import { useOccurrencesStore } from '@stores';
import { useUser } from '@supabase/auth-helpers-react';
import clsx from 'clsx';
import { format } from 'date-fns';
import { motion } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import React from 'react';

import OccurrenceChip from './OccurrenceChip';

type CalendarCellProps = {
dateNumber: number;
monthIndex: number;
monthNumber: number;
fullYear: number;
onClick: (dateNumber: number, monthIndex: number, fullYear: number) => void;
onClick: (dateNumber: number, monthNumber: number, fullYear: number) => void;
onNavigateBack?: () => void;
onNavigateForward?: () => void;
rangeStatus: 'below-range' | 'in-range' | 'above-range';
};

const CalendarCell = ({
dateNumber,
monthIndex,
monthNumber,
fullYear,
onNavigateBack,
onNavigateForward,
Expand All @@ -35,19 +35,19 @@ const CalendarCell = ({
const today = new Date();
const isToday =
today.getDate() === dateNumber &&
today.getMonth() + 1 === monthIndex &&
today.getMonth() + 1 === monthNumber &&
today.getFullYear() === fullYear;
const screenSize = useScreenSize();
const date = format(
new Date(fullYear, monthIndex - 1, dateNumber),
new Date(fullYear, monthNumber - 1, dateNumber),
'yyyy-MM-dd'
);
const occurrences = occurrencesByDate[date] || [];

const groupedOccurrences = Object.groupBy(occurrences, (o) => o.habitId);

const handleClick = React.useCallback(() => {
if (fetchingOccurrences || !user?.id) {
if (fetchingOccurrences || !user) {
return null;
}

Expand All @@ -61,18 +61,18 @@ const CalendarCell = ({
}
}

return onClick(dateNumber, monthIndex, fullYear);
return onClick(dateNumber, monthNumber, fullYear);
}, [
isToday,
dateNumber,
fetchingOccurrences,
fullYear,
monthIndex,
monthNumber,
onClick,
onNavigateBack,
onNavigateForward,
rangeStatus,
user?.id,
user,
]);

React.useEffect(() => {
Expand Down Expand Up @@ -120,7 +120,9 @@ const CalendarCell = ({
const cellRootClassName = clsx(
'flex h-28 flex-1 flex-col border-r-3 border-neutral-500 last-of-type:border-r-0 hover:bg-neutral-200 dark:border-neutral-400 dark:hover:bg-neutral-800',
rangeStatus === 'below-range' && 'cursor-w-resize',
rangeStatus === 'above-range' && 'cursor-e-resize'
rangeStatus === 'above-range' && 'cursor-e-resize',
isToday &&
'bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-800 dark:hover:bg-neutral-700'
);

const cellHeaderClassName = clsx(
Expand All @@ -132,10 +134,6 @@ const CalendarCell = ({
<div
className={cellRootClassName}
ref={cellRef}
data-is-within-active-month={rangeStatus === 'in-range'}
data-is-within-prev-month={rangeStatus === 'below-range'}
data-is-within-next-month={rangeStatus === 'above-range'}
data-is-today={isToday}
onClick={handleClick}
tabIndex={0}
role="button"
Expand All @@ -145,23 +143,29 @@ const CalendarCell = ({
{renderToday()}
</div>
<div className="flex flex-wrap gap-1 overflow-auto px-2 py-0.5 pb-1">
{Object.entries(groupedOccurrences).map(
([habitId, habitOccurrences]) => {
return (
<motion.div
key={habitId}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<OccurrenceChip
occurrences={habitOccurrences!}
onDelete={handleOccurrenceDelete}
/>
</motion.div>
);
}
)}
<AnimatePresence mode="sync">
{Object.entries(groupedOccurrences).map(
([habitId, habitOccurrences]) => {
if (!habitOccurrences) {
return null;
}

return (
<motion.div
key={habitId}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<OccurrenceChip
occurrences={habitOccurrences}
onDelete={handleOccurrenceDelete}
/>
</motion.div>
);
}
)}
</AnimatePresence>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/calendar/CalendarMonthGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const Month = (
<CalendarCell
key={dayKey}
dateNumber={day}
monthIndex={month}
monthNumber={month}
fullYear={year}
onClick={onDayModalDialogOpen}
rangeStatus={rangeStatus}
Expand Down
21 changes: 0 additions & 21 deletions src/components/calendar/OccurrenceChip.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useScreenSize } from '@hooks';
import { useOccurrencesStore } from '@stores';
import { render, waitFor } from '@testing-library/react';
import { makeTestOccurrence } from '@tests';
import { getHabitIconUrl } from '@utils';
Expand All @@ -23,9 +22,6 @@ describe(OccurrenceChip.name, () => {
};

it('should render img with habit icon', async () => {
(useOccurrencesStore as unknown as jest.Mock).mockReturnValue({
occurrenceIdBeingDeleted: null,
});
const { getByAltText } = render(<OccurrenceChip {...props} />);
const img = getByAltText('Test Habit Name icon');
expect(img).toBeInTheDocument();
Expand All @@ -38,30 +34,13 @@ describe(OccurrenceChip.name, () => {
});

it('should call onDelete when delete button is clicked', () => {
(useOccurrencesStore as unknown as jest.Mock).mockReturnValue({
occurrenceIdBeingDeleted: null,
});
const { getByRole } = render(<OccurrenceChip {...props} />);
const deleteButton = getByRole('habit-chip-delete-button');
deleteButton.click();
expect(mockOnDelete).toHaveBeenCalledWith(1, expect.anything());
});

it('should render CircularProgress when occurrence is being deleted', () => {
(useOccurrencesStore as unknown as jest.Mock).mockReturnValue({
occurrenceIdBeingDeleted: 1,
});
const { getByRole, queryByRole } = render(<OccurrenceChip {...props} />);
const loader = getByRole('habit-chip-delete-loader');
const chipDelete = queryByRole('habit-chip-delete-button');
expect(loader).toBeInTheDocument();
expect(chipDelete).toBeNull();
});

it('should not render delete button on small screens', () => {
(useOccurrencesStore as unknown as jest.Mock).mockReturnValue({
occurrenceIdBeingDeleted: null,
});
(useScreenSize as jest.Mock).mockReturnValue(1024);
const { queryByRole } = render(<OccurrenceChip {...props} />);
const chipDelete = queryByRole('habit-chip-delete-button');
Expand Down
13 changes: 2 additions & 11 deletions src/components/calendar/OccurrenceChip.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useScreenSize } from '@hooks';
import type { Occurrence } from '@models';
import { Spinner, Chip, Button, Tooltip, Badge } from '@nextui-org/react';
import { Chip, Button, Tooltip, Badge } from '@nextui-org/react';
import { X } from '@phosphor-icons/react';
import { useOccurrencesStore } from '@stores';
import { getHabitIconUrl } from '@utils';
import React from 'react';

Expand All @@ -20,15 +19,12 @@ const OccurrenceChip = ({
onDelete,
colorOverride,
}: OccurrenceChipProps) => {
const { occurrenceIdBeingDeleted } = useOccurrencesStore();
const [{ id, habit }] = occurrences;
const [{ habit }] = occurrences;
const { name: habitName, iconPath, trait } = habit || {};
const { color: traitColor } = trait || {};
const screenSize = useScreenSize();
const iconUrl = getHabitIconUrl(iconPath);

const isBeingDeleted = occurrenceIdBeingDeleted === id;

const chipStyle = {
backgroundColor: colorOverride || traitColor,
};
Expand All @@ -42,10 +38,6 @@ const OccurrenceChip = ({
return null;
}

if (isBeingDeleted) {
return <Spinner size="sm" role="habit-chip-delete-loader" />;
}

return (
<Button
isIconOnly
Expand All @@ -70,7 +62,6 @@ const OccurrenceChip = ({
size="sm"
role="habit-chip"
startContent={startContent}
isDisabled={isBeingDeleted}
endContent={getEndContent()}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/Snackbars/Snackbars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Snackbars = () => {

return (
<div className="fixed bottom-2 left-2 z-[99] flex flex-col gap-2">
<AnimatePresence mode="wait">
<AnimatePresence mode="sync">
{snackbars.map(({ id, message, options }) => {
const {
action,
Expand Down
9 changes: 4 additions & 5 deletions src/stores/occurrences.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ type OccurrencesState = {
allOccurrences: Occurrence[];
occurrences: Occurrence[];
occurrencesByDate: OccurrencesDateMap;
occurrenceIdBeingDeleted: number;
filteredBy: OccurrenceFilters;
range: [number, number];
fetchOccurrences: () => Promise<void>;
Expand Down Expand Up @@ -53,7 +52,6 @@ const useOccurrencesStore = create<OccurrencesState>((set, get) => {
allOccurrences: [],
occurrences: [],
occurrencesByDate: {},
occurrenceIdBeingDeleted: 0,
filteredBy: {
habitIds: new Set(habits.map((habit) => habit.id.toString())),
traitIds: new Set(traits.map((trait) => trait.id.toString())),
Expand Down Expand Up @@ -118,7 +116,6 @@ const useOccurrencesStore = create<OccurrencesState>((set, get) => {
removeOccurrence: async (id: number) => {
const { range } = get();
try {
set({ occurrenceIdBeingDeleted: id });
await destroyOccurrence(id);
set((state) => ({
allOccurrences: state.allOccurrences.filter(
Expand All @@ -139,8 +136,6 @@ const useOccurrencesStore = create<OccurrencesState>((set, get) => {
}
);
console.error(error);
} finally {
set({ occurrenceIdBeingDeleted: 0 });
}
},
removeOccurrencesByHabitId: (habitId: number) => {
Expand Down Expand Up @@ -213,6 +208,10 @@ useOccurrencesStore.subscribe((state, prevState) => {
) {
state.updateOccurrences(state.allOccurrences, state.filteredBy);
}

if (prevState.range !== state.range) {
void state.fetchOccurrences();
}
});

export default useOccurrencesStore;

0 comments on commit ec53622

Please sign in to comment.