Skip to content

Commit

Permalink
feat: add week numbers & improve occurrence tooltips (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
domhhv authored Dec 29, 2024
1 parent 581b2eb commit 471ee60
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 123 deletions.
2 changes: 2 additions & 0 deletions src/components/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ const Calendar = () => {
onResetFocusedDate={resetFocusedDate}
/>
<CalendarGrid
activeMonthLabel={capitalizeFirstLetter(activeMonthLabel)}
activeYear={Number(activeYear)}
state={state}
onDayModalDialogOpen={handleDayModalDialogOpen}
/>
Expand Down
4 changes: 3 additions & 1 deletion src/components/calendar/CalendarCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ export type CellPosition =
| 'bottom-right'
| '';

export type CellRangeStatus = 'below-range' | 'in-range' | 'above-range';

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

Expand Down
12 changes: 11 additions & 1 deletion src/components/calendar/CalendarGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { capitalizeFirstLetter } from '@utils';
import React from 'react';
import { useCalendarGrid } from 'react-aria';
import { type CalendarState } from 'react-stately';
Expand All @@ -11,11 +12,18 @@ type CalendarGridProps = {
monthIndex: number,
fullYear: number
) => void;
activeMonthLabel: string;
activeYear: number;
};

const WEEK_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];

const CalendarGrid = ({ state, onDayModalDialogOpen }: CalendarGridProps) => {
const CalendarGrid = ({
state,
onDayModalDialogOpen,
activeMonthLabel,
activeYear,
}: CalendarGridProps) => {
const { gridProps } = useCalendarGrid({}, state);

return (
Expand All @@ -36,6 +44,8 @@ const CalendarGrid = ({ state, onDayModalDialogOpen }: CalendarGridProps) => {
<CalendarMonthGrid
onDayModalDialogOpen={onDayModalDialogOpen}
state={state}
activeMonthLabel={capitalizeFirstLetter(activeMonthLabel)}
activeYear={activeYear}
/>
</div>
);
Expand Down
107 changes: 66 additions & 41 deletions src/components/calendar/CalendarMonthGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { getYearWeekNumberFromMonthWeek } from '@helpers';
import { type CalendarDate, getWeeksInMonth } from '@internationalized/date';
import { Button } from '@nextui-org/react';
import clsx from 'clsx';
import React, { type ForwardedRef } from 'react';
import { useLocale } from 'react-aria';
import { type CalendarState } from 'react-stately';

import CalendarCell from './CalendarCell';
import type { CellPosition, CellRangeStatus } from './CalendarCell';

type MonthProps = {
state: CalendarState;
Expand All @@ -13,18 +16,23 @@ type MonthProps = {
monthIndex: number,
fullYear: number
) => void;
activeMonthLabel: string;
activeYear: number;
};

const Month = (
{ state, onDayModalDialogOpen }: MonthProps,
{ state, onDayModalDialogOpen, activeMonthLabel, activeYear }: MonthProps,
ref: ForwardedRef<HTMLDivElement>
) => {
const { locale } = useLocale();
const weeksInMonthCount = getWeeksInMonth(state.visibleRange.start, locale);
const weekIndexes = [...new Array(weeksInMonthCount).keys()];
const { month: activeMonth } = state.visibleRange.start;

const getCellPosition = (weekIndex: number, dayIndex: number) => {
const getCellPosition = (
weekIndex: number,
dayIndex: number
): CellPosition => {
if (weekIndex === 0 && dayIndex === 0) {
return 'top-left';
}
Expand All @@ -47,48 +55,65 @@ const Month = (
return (
<div ref={ref} className="flex flex-1 flex-col">
{weekIndexes.map((weekIndex) => {
const weekContainerClassName = clsx(
'flex h-[110px] justify-between border-l-2 border-r-2 border-t-2 border-neutral-500 last-of-type:border-b-2 dark:border-neutral-400 lg:h-auto',
weekIndex === 0 && 'rounded-t-lg',
weekIndex === weeksInMonthCount - 1 && 'rounded-b-lg'
const weekNum = getYearWeekNumberFromMonthWeek(
activeMonthLabel,
activeYear,
weekIndex
);

return (
<div key={weekIndex} className={weekContainerClassName}>
{state
.getDatesInWeek(weekIndex)
.map((calendarDate: CalendarDate | null, dayIndex) => {
if (!calendarDate) {
return null;
}

const { month, day, year } = calendarDate;

const rangeStatus =
month < activeMonth
? 'below-range'
: month > activeMonth
? 'above-range'
: 'in-range';

const [cellKey] = calendarDate.toString().split('T');

const position = getCellPosition(weekIndex, dayIndex);

return (
<CalendarCell
key={cellKey}
dateNumber={day}
monthNumber={month}
fullYear={year}
onClick={onDayModalDialogOpen}
rangeStatus={rangeStatus}
position={position}
onNavigateBack={state.focusPreviousPage}
onNavigateForward={state.focusNextPage}
/>
);
})}
<div key={weekIndex} className="group flex items-center gap-4">
<Button
className={clsx(
'h-[110px] basis-[40px]',
'hidden' // TODO: show the week number button, open weekly view (WIP) on click
)}
variant="ghost"
>
{weekNum}
</Button>
<div
className={clsx(
'flex h-[110px] w-full basis-full justify-between border-l-2 border-r-2 border-neutral-500 last-of-type:border-b-2 group-first-of-type:border-t-2 dark:border-neutral-400 lg:h-auto',
weekIndex === 0 && 'rounded-t-lg',
weekIndex === weeksInMonthCount - 1 && 'rounded-b-lg'
)}
>
{state
.getDatesInWeek(weekIndex)
.map((calendarDate: CalendarDate | null, dayIndex) => {
if (!calendarDate) {
return null;
}

const { month, day, year } = calendarDate;

const rangeStatus: CellRangeStatus =
month < activeMonth
? 'below-range'
: month > activeMonth
? 'above-range'
: 'in-range';

const [cellKey] = calendarDate.toString().split('T');

const position = getCellPosition(weekIndex, dayIndex);

return (
<CalendarCell
key={cellKey}
dateNumber={day}
monthNumber={month}
fullYear={year}
onClick={onDayModalDialogOpen}
rangeStatus={rangeStatus}
position={position}
onNavigateBack={state.focusPreviousPage}
onNavigateForward={state.focusNextPage}
/>
);
})}
</div>
</div>
);
})}
Expand Down
130 changes: 75 additions & 55 deletions src/components/calendar/OccurrenceChip.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Occurrence } from '@models';
import { Badge, Button, Tooltip } from '@nextui-org/react';
import { Trash } from '@phosphor-icons/react';
import { Note, Trash } from '@phosphor-icons/react';
import { useNotesStore } from '@stores';
import { getHabitIconUrl } from '@utils';
import { format } from 'date-fns';
Expand Down Expand Up @@ -40,78 +40,98 @@ const OccurrenceChip = ({
};

const tooltipContent = (
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<span className="font-bold">{habitName}</span>
<Button
isIconOnly
variant="solid"
color="danger"
onClick={(clickEvent) => onDelete(occurrences[0].id, clickEvent)}
role="habit-chip-delete-button"
className="h-6 w-6 min-w-0 rounded-lg"
>
<Trash size={14} fill="bold" className="fill-white" />
</Button>
</div>
<ul className="italic">
{occurrenceTimes.map((t) => (
<li key={t.occurrenceId}>
<span className="font-semibold">{t.time}</span>:{' '}
<span className="font-normal">
{occurrenceNotes.find((n) => n.occurrenceId === t.occurrenceId)
?.content || 'No note'}
</span>
</li>
))}
<div className="max-h-96 space-y-2 overflow-x-hidden p-1">
<span className="font-bold">{habitName}</span>
<ul className="space-y-2 italic">
{occurrenceTimes.map((t) => {
const note = occurrenceNotes.find(
(n) => n.occurrenceId === t.occurrenceId
);
return (
<li
key={t.occurrenceId}
className="mb-2 flex items-start justify-between gap-4 border-b border-neutral-500 py-2"
>
<div className="w-48">
<span className="font-semibold">{t.time}</span>
{!!note && (
<span className="font-normal">: {note.content}</span>
)}
</div>
<Button
isIconOnly
variant="solid"
color="danger"
onClick={(clickEvent) => onDelete(t.occurrenceId, clickEvent)}
role="habit-chip-delete-button"
className="h-6 w-6 min-w-0 rounded-lg"
>
<Trash size={14} fill="bold" className="fill-white" />
</Button>
</li>
);
})}
</ul>
</div>
);

const renderChip = () => {
const chip = (
let chipTooltip = (
<Tooltip
isDisabled={!habitName}
content={tooltipContent}
radius="sm"
classNames={{
content: 'px-2 py-1.5',
}}
delay={0}
closeDelay={0}
>
<div
style={chipStyle}
className="relative mr-1 mt-1 min-w-0 rounded-full border-2 bg-slate-100 p-2 dark:bg-slate-800"
role="habit-chip"
onClick={(e) => {
e.stopPropagation();
alert('Habit entry modal is coming soon!');
}}
>
<img
src={iconUrl}
alt={`${habitName} icon`}
className="h-5 w-5 rounded"
/>
</div>
);
</Tooltip>
);

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

return chip;
};
if (occurrenceNotes.length) {
chipTooltip = (
<Badge
size="sm"
content={<Note size={16} />}
placement="top-right"
className="top-1 border-none bg-transparent"
>
{chipTooltip}
</Badge>
);
}

return (
<Tooltip
isDisabled={!habitName}
content={tooltipContent}
radius="sm"
classNames={{
content: 'px-2 py-1.5',
}}
>
{renderChip()}
</Tooltip>
);
return chipTooltip;
};

export default OccurrenceChip;
11 changes: 11 additions & 0 deletions src/components/calendar/WeeklyCalendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';

const WeeklyCalendar = () => {
return (
<div>
<h1>Weekly Calendar</h1>
</div>
);
};

export default WeeklyCalendar;
Loading

0 comments on commit 471ee60

Please sign in to comment.