Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow adding notes to days; improve calendar ui; fix get_longest_streak fn #142

Merged
merged 1 commit into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"no-console": ["warn", { "allow": ["warn", "error"] }],
"object-shorthand": "error",
"import/no-duplicates": "error",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": [
"error",
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ To set up a local Supabase instance, run the following commands (Docker required

There are a few ways to create and run migrations in the project.

- Diffing the database schema:
- Diffing the database schema to automatically generate a new migration file:

_Do the necessary changes in the local Supabase studio and then run the following to automatically generate a new migration file:_

```bash
yarn db:diff
yarn db:diff -f <your-migration-name>
```

- Creating a new migration file manually:
Expand All @@ -179,7 +179,7 @@ _To create a new migration file manually, run the following command:_
yarn db:migration:new <your-migration-name>
```

Either way, the new migration file will be created in the `supabase/migrations` directory. You can then apply the migration by running:
Either way, the new migration file will be created in the `supabase/migrations` directory. Write/change the SQL queries in the migration file to reflect the changes you want to make to the database schema. Then, apply the migration by running:

```bash
yarn db:migration:up
Expand Down
28 changes: 23 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,29 @@
justify-content: center;
align-items: center;
height: 100%;

span {
font-size: 1.2rem;
}

img {
width: 24px;
height: 24px;
margin-right: 8px;
}
}

.loader-logo {
animation: spin 2s linear infinite;
}

.loader-container img {
margin-right: 8px;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
Expand All @@ -55,10 +74,9 @@
<img
src="/android-chrome-192x192.png"
alt="Habilify logo"
width="16"
height="16"
class="loader-logo"
/>
We're preparing the app...
<span>We're preparing the app...</span>
</div>
</div>
</body>
Expand Down
7 changes: 0 additions & 7 deletions src/components/calendar/Calendar.test.tsx

This file was deleted.

61 changes: 45 additions & 16 deletions src/components/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import React from 'react';
import { type AriaButtonProps, useCalendar, useLocale } from 'react-aria';
import { useCalendarState } from 'react-stately';

import AddOccurrenceDialog from './AddOccurrenceDialog';
import CalendarGrid from './CalendarGrid';
import CalendarHeader from './CalendarHeader';
import NoteDialog from './NoteDialog';
import OccurrenceDialog from './OccurrenceDialog';

const createCalendar = (identifier: string) => {
switch (identifier) {
Expand All @@ -32,9 +33,14 @@ const Calendar = () => {
const { calendarProps, prevButtonProps, nextButtonProps, title } =
useCalendar({}, state);
const {
isOpen: isAddOccurrenceDialogOpen,
onOpen: openAddOccurrenceDialog,
onClose: closeDayOccurrenceDialog,
isOpen: isNoteDialogOpen,
onOpen: openNoteDialog,
onClose: closeNoteDialog,
} = useDisclosure();
const {
isOpen: isOccurrenceDialogOpen,
onOpen: openOccurrenceDialog,
onClose: closeOccurrenceDialog,
} = useDisclosure();
const [activeDate, setActiveDate] = React.useState<Date | null>(null);

Expand Down Expand Up @@ -87,26 +93,42 @@ const Calendar = () => {
setFocusedDate(year, month + 1, day);
};

const handleDayModalDialogOpen = (
const calendarContainerClassName = clsx(
'flex h-full w-full max-w-full flex-1 flex-col gap-2 p-0 px-2 pb-8 lg:gap-4 lg:px-0 lg:px-16 lg:py-4',
isOccurrenceDialogOpen && 'pointer-events-none'
);

const handleOccurrenceModalClose = () => {
window.setTimeout(() => {
closeOccurrenceDialog();
setActiveDate(null);
}, 0);
};

const handleOccurrenceModalOpen = (
dateNumber: number,
monthIndex: number,
fullYear: number
) => {
openAddOccurrenceDialog();
openOccurrenceDialog();
setActiveDate(new Date(fullYear, monthIndex - 1, dateNumber, 12));
};

const handleDayModalDialogClose = () => {
const handleNoteModalClose = () => {
window.setTimeout(() => {
closeDayOccurrenceDialog();
closeNoteDialog();
setActiveDate(null);
}, 0);
};

const calendarContainerClassName = clsx(
'flex h-full w-full max-w-full flex-1 flex-col gap-2 p-0 pb-8 lg:gap-4 lg:px-16 lg:py-4',
isAddOccurrenceDialogOpen && 'pointer-events-none'
);
const handleNoteModalOpen = (
dateNumber: number,
monthIndex: number,
fullYear: number
) => {
openNoteDialog();
setActiveDate(new Date(fullYear, monthIndex - 1, dateNumber, 12));
};

return (
<>
Expand All @@ -126,13 +148,20 @@ const Calendar = () => {
activeMonthLabel={capitalizeFirstLetter(activeMonthLabel)}
activeYear={Number(activeYear)}
state={state}
onDayModalDialogOpen={handleDayModalDialogOpen}
onAddOccurrence={handleOccurrenceModalOpen}
onAddNote={handleNoteModalOpen}
/>
</div>

<AddOccurrenceDialog
isOpen={isAddOccurrenceDialogOpen}
onClose={handleDayModalDialogClose}
<OccurrenceDialog
isOpen={isOccurrenceDialogOpen}
onClose={handleOccurrenceModalClose}
date={activeDate}
/>

<NoteDialog
open={isNoteDialogOpen}
onClose={handleNoteModalClose}
date={activeDate}
/>
</>
Expand Down
7 changes: 0 additions & 7 deletions src/components/calendar/CalendarCell.test.tsx

This file was deleted.

98 changes: 79 additions & 19 deletions src/components/calendar/CalendarCell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { useScreenSize } from '@hooks';
import { CalendarBlank } from '@phosphor-icons/react';
import { useOccurrencesStore } from '@stores';
import { Button, Tooltip } from '@nextui-org/react';
import {
CalendarBlank,
CalendarPlus,
NoteBlank,
NotePencil,
} from '@phosphor-icons/react';
import { useNotesStore, useOccurrencesStore } from '@stores';
import { useUser } from '@supabase/auth-helpers-react';
import clsx from 'clsx';
import { format } from 'date-fns';
Expand All @@ -22,7 +28,16 @@ type CalendarCellProps = {
dateNumber: number;
monthNumber: number;
fullYear: number;
onClick: (dateNumber: number, monthNumber: number, fullYear: number) => void;
onAddNote: (
dateNumber: number,
monthNumber: number,
fullYear: number
) => void;
onAddOccurrence: (
dateNumber: number,
monthNumber: number,
fullYear: number
) => void;
onNavigateBack?: () => void;
onNavigateForward?: () => void;
rangeStatus: CellRangeStatus;
Expand All @@ -35,14 +50,16 @@ const CalendarCell = ({
fullYear,
onNavigateBack,
onNavigateForward,
onClick,
onAddNote,
onAddOccurrence,
rangeStatus,
position,
}: CalendarCellProps) => {
const cellRef = React.useRef<HTMLDivElement>(null);
const user = useUser();
const { removeOccurrence, fetchingOccurrences, occurrencesByDate } =
useOccurrencesStore();
const { notes, fetchingNotes } = useNotesStore();
const today = new Date();
const isToday =
today.getDate() === dateNumber &&
Expand All @@ -54,10 +71,20 @@ const CalendarCell = ({
'yyyy-MM-dd'
);
const occurrences = occurrencesByDate[date] || [];
const isMobile = screenSize < 768;
const hasNote = notes.some((note) => note.day === date);

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

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

return onAddNote(dateNumber, monthNumber, fullYear);
}, [fetchingOccurrences, user, dateNumber, monthNumber, fullYear, onAddNote]);

const handleAddOccurrenceClick = React.useCallback(() => {
if (fetchingOccurrences || !user) {
return null;
}
Expand All @@ -72,14 +99,14 @@ const CalendarCell = ({
}
}

return onClick(dateNumber, monthNumber, fullYear);
return onAddOccurrence(dateNumber, monthNumber, fullYear);
}, [
isToday,
dateNumber,
fetchingOccurrences,
fullYear,
monthNumber,
onClick,
onAddOccurrence,
onNavigateBack,
onNavigateForward,
rangeStatus,
Expand All @@ -95,7 +122,7 @@ const CalendarCell = ({

const enterHandler = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
handleClick();
handleAddOccurrenceClick();
}
};

Expand All @@ -104,7 +131,7 @@ const CalendarCell = ({
return () => {
cell.removeEventListener('keydown', enterHandler);
};
}, [cellRef, handleClick]);
}, [cellRef, handleAddOccurrenceClick]);

const handleOccurrenceDelete = async (
occurrenceId: number,
Expand All @@ -119,17 +146,15 @@ const CalendarCell = ({
return null;
}

const isMobile = screenSize < 768;

if (isMobile) {
return <CalendarBlank weight="bold" size={18} />;
return <CalendarBlank size={14} weight="bold" />;
}

return <p className="font-bold">Today</p>;
};

const cellRootClassName = clsx(
'flex h-auto flex-1 flex-col border-r-2 border-neutral-500 last-of-type:border-r-0 hover:bg-neutral-200 dark:border-neutral-400 dark:hover:bg-neutral-800 lg:h-28',
'group/cell flex h-auto flex-1 flex-col gap-2 border-r-2 border-neutral-500 transition-colors last-of-type:border-r-0 hover:bg-neutral-200 dark:border-neutral-400 dark:hover:bg-neutral-800 lg:h-28',
rangeStatus === 'below-range' && 'cursor-w-resize',
rangeStatus === 'above-range' && 'cursor-e-resize',
position === 'top-left' && 'rounded-tl-md',
Expand All @@ -141,23 +166,58 @@ const CalendarCell = ({
);

const cellHeaderClassName = clsx(
'flex items-center justify-between rounded-t px-1.5 py-0.5',
rangeStatus !== 'in-range' && 'text-neutral-400 dark:text-neutral-600'
'flex w-full items-center justify-between border-b-1 border-neutral-500 px-1.5 py-0.5 text-sm dark:border-neutral-400 md:text-base',
rangeStatus !== 'in-range' && 'text-neutral-400 dark:text-neutral-600',
isToday ? 'w-full self-auto md:self-start' : 'w-full'
);

return (
<div
className={cellRootClassName}
ref={cellRef}
onClick={handleClick}
tabIndex={0}
role="button"
onClick={isMobile ? handleAddOccurrenceClick : undefined}
>
<div className={cellHeaderClassName}>
<p className="font-bold">{dateNumber}</p>
{renderToday()}
<div className="flex items-center justify-between gap-2">
{rangeStatus === 'in-range' && !isMobile && (
<div className="flex items-center gap-1 opacity-100 transition-opacity group-hover/cell:opacity-100 md:opacity-0">
<Tooltip content="Log habit" closeDelay={0}>
<Button
className="h-5 min-w-fit px-2"
radius="sm"
onClick={handleAddOccurrenceClick}
color="primary"
isDisabled={fetchingOccurrences || !user}
>
<CalendarPlus weight="bold" size={14} />
</Button>
</Tooltip>
<Tooltip
content={hasNote ? 'Edit note' : 'Add note'}
closeDelay={0}
>
<Button
className="h-5 min-w-fit px-2"
radius="sm"
onClick={handleAddNoteClick}
color="primary"
isDisabled={fetchingNotes || !user}
>
{hasNote ? (
<NotePencil weight="bold" size={14} />
) : (
<NoteBlank weight="bold" size={14} />
)}
</Button>
</Tooltip>
</div>
)}
{renderToday()}
</div>
</div>
<div className="flex flex-wrap gap-1 overflow-auto px-2 py-0.5 pb-2">
<div className="flex flex-wrap justify-center gap-1 overflow-x-auto overflow-y-visible px-0 py-0.5 pb-2 md:justify-start md:px-2">
<AnimatePresence mode="sync">
{Object.entries(groupedOccurrences).map(
([habitId, habitOccurrences]) => {
Expand Down
7 changes: 0 additions & 7 deletions src/components/calendar/CalendarGrid.test.tsx

This file was deleted.

Loading
Loading