Skip to content

Commit

Permalink
Refactor to make the Monthly Day component composable (#4)
Browse files Browse the repository at this point in the history
* Refactor to make the Monthly Day component composable

* Fix a few more missing pieces

* remove key

* Export handleOmittedDays

* bump version
  • Loading branch information
zackify authored Apr 27, 2021
1 parent 67d4be5 commit 41264e2
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 176 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
node_modules
.cache
dist
.parcel-cache
.parcel-cache
coverage
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ npm install @zach.codes/react-calendar date-fns
[See this code in action](https://calendar.zach.codes/?path=/story/monthly-calendar--basic-monthly-calendar)

```tsx
import {format, subHours, startOfMonth} from 'date-fns'
import { format, subHours, startOfMonth } from 'date-fns';
import {
MonthlyBody,
MonthlyDay,
MonthlyCalendar,
MonthlyNav,
DefaultMonthlyEventItem,
Expand All @@ -46,17 +47,20 @@ export const MyMonthlyCalendar = () => {
{ title: 'Call John', date: subHours(new Date(), 1) },
{ title: 'Meeting with Bob', date: new Date() },
]}
renderDay={data =>
data.map((item, index) => (
<DefaultMonthlyEventItem
key={index}
title={item.title}
// Format the date here to be in the format you prefer
date={format(item.date, 'k:mm')}
/>
))
}
/>
>
<MonthlyDay<EventType>
renderDay={data =>
data.map((item, index) => (
<DefaultMonthlyEventItem
key={index}
title={item.title}
// Format the date here to be in the format you prefer
date={format(item.date, 'k:mm')}
/>
))
}
/>
</MonthlyBody>
</MonthlyCalendar>
);
};
Expand Down Expand Up @@ -105,6 +109,9 @@ No props at this time

- `omitDays` lets you hide certain days from the calendar, for instance, hide Saturday and Sunday. Days are represented as 0-6, as seen in the [date-fns](https://date-fns.org/v2.19.0/docs/getDay#returns) documentation. Hiding Monday would be `omitDays={[1]}` Hiding the weekend would be `omitDays={[0, 6]}`
- `events` this is an array of events, the only thing required inside each object is a `date` field with a Date object representing the exact time of the event

`MonthlyDay`

- `renderDay` callback function that is passed an array of events for each day displayed, letting you render the events for the day

## WeeklyCalendar
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.0.3",
"version": "0.1.0",
"license": "MIT",
"name": "@zach.codes/react-calendar",
"main": "dist/index.js",
Expand Down
135 changes: 135 additions & 0 deletions src/Monthly/MonthlyBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { ReactNode, useContext } from 'react';
import { useMonthlyCalendar } from './MonthlyCalendar';
import { daysInWeek } from '../shared';
import { format, getDay, isSameDay } from 'date-fns';

const MonthlyBodyContext = React.createContext({} as any);
type BodyState<DayData> = {
day: Date;
events: DayData[];
};

export function useMonthlyBody<DayData>() {
return useContext<BodyState<DayData>>(MonthlyBodyContext);
}

type OmittedDaysProps = {
days: Date[];
omitDays?: number[];
};

export const handleOmittedDays = ({ days, omitDays }: OmittedDaysProps) => {
let headings = daysInWeek;
let daysToRender = days;

//omit the headings and days of the week that were passed in
if (omitDays) {
headings = daysInWeek.filter(day => !omitDays.includes(day.day));
daysToRender = days.filter(day => !omitDays.includes(getDay(day)));
}

// omit the padding if an omitted day was before the start of the month
let firstDayOfMonth = getDay(daysToRender[0]) as number;
if (omitDays) {
let subtractOmittedDays = omitDays.filter(day => day < firstDayOfMonth)
.length;
firstDayOfMonth = firstDayOfMonth - subtractOmittedDays;
}
let padding = new Array(firstDayOfMonth).fill(0);

return { headings, daysToRender, padding };
};

//to prevent these from being purged in production, we make a lookup object
const headingClasses = {
l3: 'lg:grid-cols-3',
l4: 'lg:grid-cols-4',
l5: 'lg:grid-cols-5',
l6: 'lg:grid-cols-6',
l7: 'lg:grid-cols-7',
};

type MonthlyBodyProps<DayData> = {
/*
skip days, an array of days, starts at sunday (0), saturday is 6
ex: [0,6] would remove sunday and saturday from rendering
*/
omitDays?: number[];
events: (DayData & { date: Date })[];
children: ReactNode;
};

export function MonthlyBody<DayData>({
omitDays,
events,
children,
}: MonthlyBodyProps<DayData>) {
let { days } = useMonthlyCalendar();
let { headings, daysToRender, padding } = handleOmittedDays({
days,
omitDays,
});

let headingClassName = 'border-b-2 p-2 border-r-2 lg:block hidden';
return (
<div className="bg-white border-l-2 border-t-2">
<div
className={`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 ${
//@ts-ignore
headingClasses[`l${headings.length}`]
}`}
>
{headings.map(day => (
<div
key={day.day}
className={headingClassName}
aria-label="Day of Week"
>
{day.label}
</div>
))}
{padding.map((_, index) => (
<div
key={index}
className={headingClassName}
aria-label="Empty Day"
/>
))}
{daysToRender.map(day => (
<MonthlyBodyContext.Provider
key={day.toISOString()}
value={{
day,
events: events.filter(data => isSameDay(data.date, day)),
}}
>
{children}
</MonthlyBodyContext.Provider>
))}
</div>
</div>
);
}

type MonthlyDayProps<DayData> = {
renderDay: (events: DayData[]) => ReactNode;
};
export function MonthlyDay<DayData>({ renderDay }: MonthlyDayProps<DayData>) {
let { day, events } = useMonthlyBody<DayData>();
let dayNumber = format(day, 'd');

return (
<div
aria-label={`Events for day ${dayNumber}`}
className="h-48 p-2 border-b-2 border-r-2"
>
<div className="flex justify-between">
<div className="font-bold">{dayNumber}</div>
<div className="lg:hidden block">{format(day, 'EEEE')}</div>
</div>
<ul className="divide-gray-200 divide-y overflow-hidden max-h-36 overflow-y-auto">
{renderDay(events)}
</ul>
</div>
);
}
111 changes: 0 additions & 111 deletions src/Monthly/MonthlyCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import {
subMonths,
addMonths,
getYear,
getDay,
isSameDay,
} from 'date-fns';
import React, { ReactNode, useContext } from 'react';
import { daysInWeek } from '../shared';

type CalendarState = {
days: Date[];
Expand Down Expand Up @@ -80,111 +77,3 @@ export const MonthlyNav = () => {
</div>
);
};
type ExtraMonthData = { date: Date };

type MonthlyBodyProps<MonthData> = {
/*
skip days, an array of days, starts at sunday (0), saturday is 6
ex: [0,6] would remove sunday and saturday from rendering
*/
omitDays?: number[];
events: (MonthData & ExtraMonthData)[];

renderDay: (data: (MonthData & ExtraMonthData)[]) => ReactNode;
};

type OmittedDaysProps = {
days: Date[];
omitDays?: number[];
};

const handleOmittedDays = ({ days, omitDays }: OmittedDaysProps) => {
let headings = daysInWeek;
let daysToRender = days;

//omit the headings and days of the week that were passed in
if (omitDays) {
headings = daysInWeek.filter(day => !omitDays.includes(day.day));
daysToRender = days.filter(day => !omitDays.includes(getDay(day)));
}

// omit the padding if an omitted day was before the start of the month
let firstDayOfMonth = getDay(daysToRender[0]) as number;
if (omitDays) {
let subtractOmittedDays = omitDays.filter(day => day < firstDayOfMonth)
.length;
firstDayOfMonth = firstDayOfMonth - subtractOmittedDays;
}
let padding = new Array(firstDayOfMonth).fill(0);

return { headings, daysToRender, padding };
};

//to prevent these from being purged in production, we make a lookup object
const headingClasses = {
l3: 'lg:grid-cols-3',
l4: 'lg:grid-cols-4',
l5: 'lg:grid-cols-5',
l6: 'lg:grid-cols-6',
l7: 'lg:grid-cols-7',
};

export function MonthlyBody<MonthData>({
omitDays,
events,
renderDay,
}: MonthlyBodyProps<MonthData>) {
let { days } = useMonthlyCalendar();
let { headings, daysToRender, padding } = handleOmittedDays({
days,
omitDays,
});

let headingClassName = 'border-b-2 p-2 border-r-2 lg:block hidden';
return (
<div className="bg-white border-l-2 border-t-2">
<div
className={`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 ${
//@ts-ignore
headingClasses[`l${headings.length}`]
}`}
>
{headings.map(day => (
<div
key={day.day}
className={headingClassName}
aria-label="Day of Week"
>
{day.label}
</div>
))}
{padding.map((_, index) => (
<div
key={index}
className={headingClassName}
aria-label="Empty Day"
/>
))}
{daysToRender.map(day => {
let dayData = events.filter(data => isSameDay(data.date, day));
let dayNumber = format(day, 'd');
return (
<div
key={day.toISOString()}
aria-label={`Events for day ${dayNumber}`}
className="h-48 p-2 border-b-2 border-r-2"
>
<div className="flex justify-between">
<div className="font-bold">{dayNumber}</div>
<div className="lg:hidden block">{format(day, 'EEEE')}</div>
</div>
<ul className="divide-gray-200 divide-y overflow-hidden max-h-36 overflow-y-auto">
{renderDay(dayData)}
</ul>
</div>
);
})}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Monthly/MonthlyCalendar';
export * from './Monthly/MonthlyBody';
export * from './Monthly/MonthlyEventItems';

export * from './Weekly/WeeklyCalendar';
Expand Down
Loading

0 comments on commit 41264e2

Please sign in to comment.