Skip to content

Commit

Permalink
feat(calendar): group occurrences by habit id inside cell (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
domhhv authored Oct 16, 2024
1 parent cb1ec54 commit e0efe84
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 60 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"supabase": "1.206.0",
"tailwindcss": "^3.4.10",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3",
"typescript": "5.6.3",
"vite": "^5.4.8"
}
}
39 changes: 22 additions & 17 deletions src/components/calendar/CalendarCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,22 @@ const CalendarCell = ({
}: CalendarCellProps) => {
const cellRef = React.useRef<HTMLDivElement>(null);
const user = useUser();
const { removeOccurrence, fetchingOccurrences } = useOccurrences();
const { removeOccurrence, fetchingOccurrences, occurrencesByDate } =
useOccurrences();
const today = new Date();
const isToday =
today.getDate() === dateNumber &&
today.getMonth() + 1 === monthIndex &&
today.getFullYear() === fullYear;
const screenSize = useScreenSize();
const { occurrencesByDate } = useOccurrences();
const date = format(
new Date(fullYear, monthIndex - 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) {
return null;
Expand Down Expand Up @@ -142,21 +144,24 @@ const CalendarCell = ({
<p className="font-bold">{dateNumber}</p>
{renderToday()}
</div>
<div className="flex flex-wrap gap-1 overflow-auto px-1 py-0.5">
{occurrences.map((occurrence) => {
return (
<motion.div
key={occurrence.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<OccurrenceChip
occurrence={occurrence}
onDelete={handleOccurrenceDelete}
/>
</motion.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 }}
>
<OccurrenceChip
occurrences={habitOccurrences!}
habitId={+habitId}
onDelete={handleOccurrenceDelete}
/>
</motion.div>
);
}
)}
</div>
</div>
);
Expand Down
23 changes: 13 additions & 10 deletions src/components/calendar/OccurrenceChip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ jest.mock('@context', () => ({
describe(OccurrenceChip.name, () => {
const mockOnDelete = jest.fn();
const props: OccurrenceChipProps = {
occurrence: {
id: 1,
createdAt: '2021-01-01T00:00:00Z',
updatedAt: '2021-01-02T00:00:00Z',
timestamp: 1612137600000,
day: '2021-02-01',
time: null,
habitId: 2,
userId: '3',
},
occurrences: [
{
id: 1,
createdAt: '2021-01-01T00:00:00Z',
updatedAt: '2021-01-02T00:00:00Z',
timestamp: 1612137600000,
day: '2021-02-01',
time: null,
habitId: 2,
userId: '3',
},
],
habitId: 2,
onDelete: mockOnDelete,
};

Expand Down
46 changes: 34 additions & 12 deletions src/components/calendar/OccurrenceChip.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useHabits, useOccurrences } from '@context';
import { useHabitTraitChipColor, useScreenSize } from '@hooks';
import type { Occurrence } from '@models';
import { Spinner, Chip, Button, Tooltip } from '@nextui-org/react';
import { Spinner, Chip, Button, Tooltip, Badge } from '@nextui-org/react';
import { X } from '@phosphor-icons/react';
import { getHabitIconUrl } from '@utils';
import React from 'react';

export type OccurrenceChipProps = {
occurrence: Occurrence;
occurrences: Occurrence[];
habitId: number;
onDelete: (
occurrenceId: number,
clickEvent: React.MouseEvent<HTMLButtonElement>
Expand All @@ -16,20 +17,19 @@ export type OccurrenceChipProps = {
};

const OccurrenceChip = ({
occurrence,
occurrences,
habitId,
onDelete,
colorOverride,
}: OccurrenceChipProps) => {
const { habitsMap } = useHabits();
const { occurrenceIdBeingDeleted } = useOccurrences();
const occurrenceHabit = habitsMap[occurrence.habitId!] || {};
const traitChipColor = useHabitTraitChipColor(
habitsMap[occurrence.habitId]?.traitId
);
const occurrenceHabit = habitsMap[habitId] || {};
const traitChipColor = useHabitTraitChipColor(occurrenceHabit.traitId);
const screenSize = useScreenSize();
const iconUrl = getHabitIconUrl(occurrenceHabit.iconPath);

const isBeingDeleted = occurrenceIdBeingDeleted === occurrence.id;
const isBeingDeleted = occurrenceIdBeingDeleted === occurrences[0].id;

const chipStyle = {
backgroundColor: colorOverride || traitChipColor,
Expand Down Expand Up @@ -58,7 +58,7 @@ const OccurrenceChip = ({
radius="full"
variant="solid"
size="sm"
onClick={(clickEvent) => onDelete(occurrence.id, clickEvent)}
onClick={(clickEvent) => onDelete(occurrences[0].id, clickEvent)}
role="habit-chip-delete-button"
className="h-4 w-4 min-w-0"
>
Expand All @@ -67,18 +67,40 @@ const OccurrenceChip = ({
);
};

return (
<Tooltip isDisabled={!occurrenceHabit.name} content={occurrenceHabit.name}>
const renderChip = () => {
const chip = (
<Chip
style={chipStyle}
className="mr-0.5 mt-0.5 min-w-0 px-1 py-0.5"
className="mr-1 mt-1 min-w-0 px-1 py-0.5"
variant="solid"
size="sm"
role="habit-chip"
startContent={startContent}
isDisabled={isBeingDeleted}
endContent={getEndContent()}
/>
);

if (occurrences.length > 1) {
return (
<Badge
size="sm"
content={occurrences.length}
variant="solid"
placement="bottom-right"
color="primary"
>
{chip}
</Badge>
);
}

return chip;
};

return (
<Tooltip isDisabled={!occurrenceHabit.name} content={occurrenceHabit.name}>
{renderChip()}
</Tooltip>
);
};
Expand Down
3 changes: 2 additions & 1 deletion src/components/habit/add-trait/AddCustomTraitModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ const AddCustomTraitModal = ({ open, onClose }: AddCustomTraitModalProps) => {
This is how habits of this trait will appear on your calendar
</p>
<OccurrenceChip
occurrence={makeTestOccurrence()}
occurrences={[makeTestOccurrence()]}
habitId={1}
onDelete={() => null}
colorOverride={`#${color}`}
/>
Expand Down
30 changes: 19 additions & 11 deletions src/context/Occurrences/OccurrencesProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
destroyOccurrence,
listOccurrences,
} from '@services';
import { cache } from '@utils';
import React, { type ReactNode } from 'react';

type Props = {
Expand Down Expand Up @@ -45,7 +46,6 @@ const OccurrencesProvider = ({ children, rangeStart, rangeEnd }: Props) => {
const fetchOccurrences = React.useCallback(async () => {
setFetchingOccurrences(true);
const result = await listOccurrences([rangeStart, rangeEnd]);
setOccurrences(result);
setFetchingOccurrences(false);
setAllOccurrences(result);
}, [rangeStart, rangeEnd]);
Expand Down Expand Up @@ -91,7 +91,7 @@ const OccurrencesProvider = ({ children, rangeStart, rangeEnd }: Props) => {
}, [filteredBy, allOccurrences, habits]);

React.useEffect(() => {
const occurrencesByDate = occurrences.reduce(
const nextOccurrencesByDate = occurrences.reduce(
(acc, occurrence) => {
const { day } = occurrence;
if (!acc[day]) {
Expand All @@ -104,7 +104,7 @@ const OccurrencesProvider = ({ children, rangeStart, rangeEnd }: Props) => {
{} as Record<string, Occurrence[]>
);

setOccurrencesByDate((prev) => ({ ...prev, ...occurrencesByDate }));
setOccurrencesByDate(nextOccurrencesByDate);
}, [occurrences]);

const filterBy = React.useCallback((options: OccurrenceFilters) => {
Expand All @@ -118,10 +118,15 @@ const OccurrencesProvider = ({ children, rangeStart, rangeEnd }: Props) => {

const nextOccurrence = await createOccurrence(occurrence);

setOccurrences((prevOccurrences) => [
setAllOccurrences((prevOccurrences) => [
...prevOccurrences,
nextOccurrence,
]);

cache.set([rangeStart, rangeEnd].toString(), [
...cache.get([rangeStart, rangeEnd].toString()),
nextOccurrence,
]);
} catch (e) {
showSnackbar('Something went wrong while adding your habit', {
color: 'danger',
Expand All @@ -133,7 +138,7 @@ const OccurrencesProvider = ({ children, rangeStart, rangeEnd }: Props) => {
setAddingOccurrence(false);
}
},
[showSnackbar]
[showSnackbar, rangeStart, rangeEnd]
);

const removeOccurrence = React.useCallback(
Expand All @@ -143,17 +148,20 @@ const OccurrencesProvider = ({ children, rangeStart, rangeEnd }: Props) => {

await destroyOccurrence(id);

setOccurrences((prevOccurrences) => {
setAllOccurrences((prevOccurrences) => {
return prevOccurrences.filter((occurrence) => {
return occurrence.id !== id;
});
});

setAllOccurrences((prevOccurrences) => {
return prevOccurrences.filter((occurrence) => {
const cachedOccurrences = cache.get([rangeStart, rangeEnd].toString());

cache.set(
[rangeStart, rangeEnd].toString(),
cachedOccurrences.filter((occurrence: Occurrence) => {
return occurrence.id !== id;
});
});
})
);

showSnackbar('Your habit entry has been deleted from the calendar.', {
dismissible: true,
Expand All @@ -169,7 +177,7 @@ const OccurrencesProvider = ({ children, rangeStart, rangeEnd }: Props) => {
setOccurrenceIdBeingDeleted(0);
}
},
[showSnackbar]
[showSnackbar, rangeStart, rangeEnd]
);

const removeOccurrencesByHabitId = (habitId: number) => {
Expand Down
4 changes: 0 additions & 4 deletions src/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export const cache = new Map();

export const store = (key: unknown, value: unknown) => {
cache.set(key, value);
};
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9310,10 +9310,10 @@ typed-array-length@^1.0.5:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"

typescript@^5.3.3:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
typescript@5.6.3:
version "5.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==

unbox-primitive@^1.0.2:
version "1.0.2"
Expand Down

0 comments on commit e0efe84

Please sign in to comment.