diff --git a/.eslintrc b/.eslintrc index 90d56f5..5590936 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,6 +29,7 @@ } }, "rules": { + "object-shorthand": "error", "import/order": [ "error", { diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 377da44..4611fb6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ # .github/workflows/lint.yml -name: Lint # name of the action (displayed in the github interface) +name: Code health checks # name of the action (displayed in the github interface) on: # event list pull_request: # on a pull request to each of these branches @@ -26,5 +26,11 @@ jobs: # list of things to do - name: Install Dependencies run: yarn install + - name: Type Checking + run: yarn typecheck + - name: Code Linting - run: yarn eslint:check \ No newline at end of file + run: yarn eslint:check + + - name: Code Formatting + run: yarn prettier:check \ No newline at end of file diff --git a/src/components/calendar/CalendarCell.tsx b/src/components/calendar/CalendarCell.tsx index 64fd4dd..4262c5f 100644 --- a/src/components/calendar/CalendarCell.tsx +++ b/src/components/calendar/CalendarCell.tsx @@ -31,29 +31,34 @@ const StyledCalendarDayCellButton = styled('button')(() => ({ '&:last-of-type': { borderRight: '1px solid', }, - '&[data-prev-month="true"]': { + '&[data-prev-month="true"]:not([disabled])': { cursor: 'w-resize', }, - '&[data-next-month="true"]': { + '&[data-next-month="true"]:not([disabled])': { cursor: 'e-resize', }, '&[data-prev-month="true"], &[data-next-month="true"]': { backgroundColor: 'white', - '&:hover': { + '&:not([disabled]):hover': { backgroundColor: '#f5f5f4', }, }, '&[data-active="true"]': { - cursor: 'pointer', backgroundColor: '#f5f5f4', - '&:hover': { - backgroundColor: '#e7e5e4', + '&:not([disabled])': { + cursor: 'pointer', + '&:hover': { + backgroundColor: '#e7e5e4', + }, }, }, '&[data-current="true"]': { backgroundColor: '#e7e5e4', - '&:hover': { - backgroundColor: '#d6d3d1', + '&:not([disabled])': { + cursor: 'pointer', + '&:hover': { + backgroundColor: '#d6d3d1', + }, }, }, })); @@ -87,7 +92,9 @@ export default function CalendarCell({ onClick, rangeStatus, }: Props) { - const { setCalendarEvents } = React.useContext(CalendarEventsContext); + const { removeCalendarEvent, fetchingCalendarEvents } = React.useContext( + CalendarEventsContext + ); const [active, setActive] = React.useState(false); const [current, setCurrent] = React.useState(false); const [eventIdBeingDeleted, setEventIdBeingDeleted] = React.useState< @@ -129,9 +136,7 @@ export default function CalendarCell({ try { await deleteCalendarEvent(calendarEventId); - setCalendarEvents((prevCalendarEvents) => - prevCalendarEvents.filter((event) => event.id !== calendarEventId) - ); + removeCalendarEvent(calendarEventId); } catch (error) { console.error(error); } finally { @@ -146,6 +151,7 @@ export default function CalendarCell({ data-next-month={rangeStatus === 'above-range'} data-current={current} onClick={handleClick} + disabled={fetchingCalendarEvents} > diff --git a/src/components/calendar/CalendarHeader.tsx b/src/components/calendar/CalendarHeader.tsx index 7e0dd2a..a44769c 100644 --- a/src/components/calendar/CalendarHeader.tsx +++ b/src/components/calendar/CalendarHeader.tsx @@ -1,3 +1,4 @@ +import { CalendarEventsContext } from '@context'; import { NavigateBefore, NavigateNext } from '@mui/icons-material'; import { Typography, styled, IconButton } from '@mui/joy'; import { AnimatePresence, motion } from 'framer-motion'; @@ -10,6 +11,7 @@ const StyledCalendarHeader = styled('div')(({ theme }) => ({ border: '1px solid', borderRadius: theme.radius.sm, alignItems: 'center', + position: 'relative', })); const StyledCalendarActiveMonthContainer = styled('div')({ @@ -30,6 +32,19 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({ }, })); +const StyledLoadingOverlay = styled(Typography)(() => ({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + margin: 'auto', + backgroundColor: 'rgba(255, 255, 255, 0.8)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})); + type ButtonProps = { disabled: boolean; 'aria-label': string; @@ -52,6 +67,8 @@ export default function CalendarHeader({ onNavigateBack, onNavigateForward, }: Props) { + const { fetchingCalendarEvents } = React.useContext(CalendarEventsContext); + return ( @@ -84,6 +101,17 @@ export default function CalendarHeader({ + + {fetchingCalendarEvents && ( + + + Fetching calendar events, please wait... + + + )} ); } diff --git a/src/components/calendar/DayHabitModalDialog.tsx b/src/components/calendar/DayHabitModalDialog.tsx index 0b8f181..bd02da7 100644 --- a/src/components/calendar/DayHabitModalDialog.tsx +++ b/src/components/calendar/DayHabitModalDialog.tsx @@ -26,7 +26,7 @@ type Props = { export default function DayHabitModalDialog({ open, onClose, date }: Props) { const { habits } = React.useContext(HabitsContext); - const { setCalendarEvents } = React.useContext(CalendarEventsContext); + const { addCalendarEvent } = React.useContext(CalendarEventsContext); const [submitting, setSubmitting] = React.useState(false); const [selectedBadHabit, setSelectedBadHabit] = React.useState( null @@ -49,10 +49,7 @@ export default function DayHabitModalDialog({ open, onClose, date }: Props) { date, selectedBadHabit as number ); - setCalendarEvents((prevCalendarEvents) => [ - ...prevCalendarEvents, - newCalendarEvent, - ]); + addCalendarEvent(newCalendarEvent); } catch (error) { console.error(error); } finally { diff --git a/src/components/habit/add-habit/AddHabitDialogButton.tsx b/src/components/habit/add-habit/AddHabitDialogButton.tsx index 4ad245a..30dc4d0 100644 --- a/src/components/habit/add-habit/AddHabitDialogButton.tsx +++ b/src/components/habit/add-habit/AddHabitDialogButton.tsx @@ -19,13 +19,17 @@ import { } from '@mui/joy'; import React, { FormEventHandler } from 'react'; -export default function AddHabitDialogButton() { +type Props = { + disabled?: boolean; +}; + +export default function AddHabitDialogButton({ disabled = false }: Props) { const [open, setOpen] = React.useState(false); const [habitName, setHabitName] = React.useState(''); const [habitDescription, setHabitDescription] = React.useState(''); const [habitTrait, setHabitTrait] = React.useState<'good' | 'bad' | ''>(''); const [addingHabit, setAddingHabit] = React.useState(false); - const { setHabits } = React.useContext(HabitsContext); + const { addHabit } = React.useContext(HabitsContext); const handleDialogOpen = () => { setOpen(true); @@ -48,7 +52,7 @@ export default function AddHabitDialogButton() { setHabitName(''); setHabitDescription(''); setHabitTrait(''); - setHabits((prevHabits) => [...prevHabits, newHabit]); + addHabit(newHabit); } catch (error) { console.error(error); } finally { @@ -80,6 +84,7 @@ export default function AddHabitDialogButton() { variant="solid" startDecorator={} onClick={handleDialogOpen} + disabled={disabled} > Add habit diff --git a/src/components/habit/edit-habit/EditHabitDialog.tsx b/src/components/habit/edit-habit/EditHabitDialog.tsx index 064de79..30ca3a3 100644 --- a/src/components/habit/edit-habit/EditHabitDialog.tsx +++ b/src/components/habit/edit-habit/EditHabitDialog.tsx @@ -36,8 +36,10 @@ export default function EditHabitDialog({ const [description, setDescription] = React.useState(''); const [trait, setTrait] = React.useState<'good' | 'bad' | ''>(''); const [isUpdating, setIsUpdating] = React.useState(false); - const { setHabits } = React.useContext(HabitsContext); - const { setCalendarEvents } = React.useContext(CalendarEventsContext); + const habitsContext = React.useContext(HabitsContext); + const { updateHabitInsideCalendarEvents } = React.useContext( + CalendarEventsContext + ); React.useEffect(() => { setIsOpen(open); @@ -84,21 +86,8 @@ export default function EditHabitDialog({ description, trait: trait as 'good' | 'bad', }); - setHabits((prevHabits) => - prevHabits.map((prevHabit) => - prevHabit.id === habit.id ? updatedHabit : prevHabit - ) - ); - setCalendarEvents((prevCalendarEvents) => - prevCalendarEvents.map((prevCalendarEvent) => - prevCalendarEvent.habit.id === habit.id - ? { - ...prevCalendarEvent, - habit: updatedHabit, - } - : prevCalendarEvent - ) - ); + habitsContext.updateHabit(updatedHabit); + updateHabitInsideCalendarEvents(updatedHabit); } catch (error) { console.error(error); } finally { diff --git a/src/components/habit/view-habit/HabitItem.tsx b/src/components/habit/view-habit/HabitItem.tsx index 4c2be0b..a586c74 100644 --- a/src/components/habit/view-habit/HabitItem.tsx +++ b/src/components/habit/view-habit/HabitItem.tsx @@ -49,17 +49,14 @@ type HabitItemProps = { export default function HabitItem({ habit, onEdit }: HabitItemProps) { const [isBeingDeleted, setIsBeingDeleted] = React.useState(false); - const { setHabits } = React.useContext(HabitsContext); + const { removeHabit } = React.useContext(HabitsContext); const handleDeleteHabit = async () => { setIsBeingDeleted(true); try { await deleteHabit(habit.id); - await new Promise((resolve) => setTimeout(resolve, 1000)); - setHabits((prevHabits) => - prevHabits.filter((prevHabit) => prevHabit.id !== habit.id) - ); + removeHabit(habit.id); } catch (error) { console.error(error); } finally { diff --git a/src/components/habit/view-habit/ViewAllHabitsModalButton.tsx b/src/components/habit/view-habit/ViewAllHabitsModalButton.tsx index aec6845..5a7dcb8 100644 --- a/src/components/habit/view-habit/ViewAllHabitsModalButton.tsx +++ b/src/components/habit/view-habit/ViewAllHabitsModalButton.tsx @@ -3,6 +3,7 @@ import ViewListRoundedIcon from '@mui/icons-material/ViewListRounded'; import { Box, Button, + CircularProgress, DialogContent, DialogTitle, List, @@ -27,7 +28,11 @@ const StyledPlaceholderContainer = styled(Box)(({ theme }) => ({ margin: `${theme.spacing(1)} auto 0`, })); -export default function ViewAllHabitsModalButton() { +type Props = { + loading?: boolean; +}; + +export default function ViewAllHabitsModalButton({ loading = false }: Props) { const { habits } = React.useContext(HabitsContext); const [open, setOpen] = React.useState(false); const [isEditingHabit, setIsEditingHabit] = React.useState(false); @@ -56,10 +61,17 @@ export default function ViewAllHabitsModalButton() { diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index ccbb098..a4c1ec0 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -1,4 +1,5 @@ import { AddHabitDialogButton, ViewAllHabitsModalButton } from '@components'; +import { HabitsContext } from '@context'; import { AccountCircleOutlined } from '@mui/icons-material'; import { IconButton } from '@mui/joy'; import { styled } from '@mui/joy/styles'; @@ -29,12 +30,14 @@ const StyledAppHeaderContent = styled('div')(() => ({ })); export default function Header() { + const { fetchingHabits } = React.useContext(HabitsContext); + return ( - - + + diff --git a/src/context/CalendarEvents.tsx b/src/context/CalendarEvents.tsx index a892254..7a6e440 100644 --- a/src/context/CalendarEvents.tsx +++ b/src/context/CalendarEvents.tsx @@ -10,10 +10,11 @@ export type CalendarEvent = { }; export const CalendarEventsContext = React.createContext({ + fetchingCalendarEvents: false, calendarEvents: [] as CalendarEvent[], - setCalendarEvents: ( - _: CalendarEvent[] | ((prevHabits: CalendarEvent[]) => CalendarEvent[]) - ) => {}, + addCalendarEvent: (_: CalendarEvent) => {}, + removeCalendarEvent: (_: number) => {}, + updateHabitInsideCalendarEvents: (_: Habit) => {}, }); type Props = { @@ -21,17 +22,60 @@ type Props = { }; export default function CalendarEventsProvider({ children }: Props) { + const [fetchingCalendarEvents, setFetchingCalendarEvents] = + React.useState(false); const [calendarEvents, setCalendarEvents] = React.useState( [] ); React.useEffect(() => { - getCalendarEvents().then(setCalendarEvents); + const loadCalendarEvents = async () => { + setFetchingCalendarEvents(true); + const calendarEvents = await getCalendarEvents(); + setCalendarEvents(calendarEvents); + setFetchingCalendarEvents(false); + }; + + void loadCalendarEvents(); }, []); + const addCalendarEvent = (calendarEvent: CalendarEvent) => { + setCalendarEvents((prevCalendarEvents) => [ + ...prevCalendarEvents, + calendarEvent, + ]); + }; + + const removeCalendarEvent = (id: number) => { + setCalendarEvents((prevCalendarEvents) => + prevCalendarEvents.filter( + (prevCalendarEvent) => prevCalendarEvent.id !== id + ) + ); + }; + + const updateHabitInsideCalendarEvents = (habit: Habit) => { + setCalendarEvents((prevCalendarEvents) => + prevCalendarEvents.map((prevCalendarEvent) => + prevCalendarEvent.habit.id === habit.id + ? { + ...prevCalendarEvent, + habit, + } + : prevCalendarEvent + ) + ); + }; + const value = React.useMemo( - () => ({ calendarEvents, setCalendarEvents }), - [calendarEvents] + () => ({ + fetchingCalendarEvents, + calendarEvents, + addCalendarEvent, + removeCalendarEvent, + updateHabitInsideCalendarEvents, + }), + [calendarEvents, fetchingCalendarEvents] ); return ( diff --git a/src/context/Habits.tsx b/src/context/Habits.tsx index 43055e4..e929343 100644 --- a/src/context/Habits.tsx +++ b/src/context/Habits.tsx @@ -9,8 +9,11 @@ export type Habit = { }; export const HabitsContext = React.createContext({ + fetchingHabits: false, habits: [] as Habit[], - setHabits: (_: Habit[] | ((prevHabits: Habit[]) => Habit[])) => {}, + addHabit: (_: Habit) => {}, + removeHabit: (_: number) => {}, + updateHabit: (_: Habit) => {}, }); type Props = { @@ -18,13 +21,42 @@ type Props = { }; export default function HabitsProvider({ children }: Props) { + const [fetchingHabits, setFetchingHabits] = React.useState(false); const [habits, setHabits] = React.useState([]); React.useEffect(() => { - getHabits().then(setHabits); + const loadHabits = async () => { + setFetchingHabits(true); + const habits = await getHabits(); + setHabits(habits); + setFetchingHabits(false); + }; + + void loadHabits(); }, []); - const value = React.useMemo(() => ({ habits, setHabits }), [habits]); + const addHabit = (habit: Habit) => { + setHabits((prevHabits) => [...prevHabits, habit]); + }; + + const removeHabit = (id: number) => { + setHabits((prevHabits) => + prevHabits.filter((prevHabit) => prevHabit.id !== id) + ); + }; + + const updateHabit = (habit: Habit) => { + setHabits((prevHabits) => + prevHabits.map((prevHabit) => + prevHabit.id === habit.id ? { ...prevHabit, ...habit } : prevHabit + ) + ); + }; + + const value = React.useMemo( + () => ({ fetchingHabits, habits, addHabit, removeHabit, updateHabit }), + [habits, fetchingHabits] + ); return ( {children}