diff --git a/README.md b/README.md index f9f8530..2eb0683 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ yarn add agenda-react-native | **`monthTheme`** | Month component theme | no | | MonthThemeType | | **`theme`** | Agenda theme | no | | ThemeType | | **`locale`** | Locale | no | | LocaleType | +| **`viewType`** | Locale | no | 'month' | AgendaViewType | | **`firstDayMonday`** | Monday as first day of the week | no | false | boolean | ## License diff --git a/example/App.tsx b/example/App.tsx index ebe8b95..fa3e7a7 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { SafeAreaView, useWindowDimensions } from 'react-native'; -import { Agenda, Event, MonthThemeType } from 'react-native-agenda'; +import { Agenda, Event, MonthThemeType } from 'agenda-react-native'; const MONTH_THEME: MonthThemeType = { activeDayContentStyle: { @@ -51,6 +51,30 @@ const events: Event[] = [ endDate: new Date(2022, 8, 3, 14, 20), color: '#fa8100', }, + { + name: 'Study 2 ', + startDate: new Date(2022, 7, 28, 14, 20), + endDate: new Date(2022, 8, 3, 14, 20), + color: '#A0E7E5', + }, + { + name: 'Study 3', + startDate: new Date(2022, 7, 28, 14, 20), + endDate: new Date(2022, 8, 3, 14, 20), + color: '#B4F8C8', + }, + { + name: 'Study 4', + startDate: new Date(2022, 7, 28, 14, 20), + endDate: new Date(2022, 8, 3, 14, 20), + color: '#FBE7C6', + }, + { + name: 'Study 5', + startDate: new Date(2022, 7, 28, 14, 20), + endDate: new Date(2022, 8, 3, 14, 20), + color: '#FFAEBC', + }, { name: 'Check-in', startDate: new Date(2022, 8, 5, 14, 20), diff --git a/example/package-lock.json b/example/package-lock.json index ae0553b..67a0a29 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -10,7 +10,7 @@ "react": "18.0.0", "react-dom": "18.0.0", "react-native": "0.69.6", - "react-native-month": "^1.4.0", + "react-native-month": "^1.5.0", "react-native-web": "~0.18.9" }, "devDependencies": { @@ -14104,9 +14104,9 @@ "integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g==" }, "node_modules/react-native-month": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.4.0.tgz", - "integrity": "sha512-fOtQ6CpsHkGEqRxw1E3SR/OqZskGeIgoIgZj+xorQDlMgATXxPac4sAWqjsB7TrDUz4yi4kGxKONQjYwG+k3fg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.5.0.tgz", + "integrity": "sha512-pKJlueHhSZ2D9RnDshsAFkV28Z/IB1E+9/8nStMDJnS/UYfi/tsBbJO42xbKG4jOV0rymymnRD0xZQKOejG0eA==", "peerDependencies": { "react": "*", "react-native": "*" @@ -29123,9 +29123,9 @@ "integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g==" }, "react-native-month": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.4.0.tgz", - "integrity": "sha512-fOtQ6CpsHkGEqRxw1E3SR/OqZskGeIgoIgZj+xorQDlMgATXxPac4sAWqjsB7TrDUz4yi4kGxKONQjYwG+k3fg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.5.0.tgz", + "integrity": "sha512-pKJlueHhSZ2D9RnDshsAFkV28Z/IB1E+9/8nStMDJnS/UYfi/tsBbJO42xbKG4jOV0rymymnRD0xZQKOejG0eA==", "requires": {} }, "react-native-web": { diff --git a/example/package.json b/example/package.json index 2213fda..1ff8b9e 100644 --- a/example/package.json +++ b/example/package.json @@ -12,7 +12,7 @@ "react": "18.0.0", "react-dom": "18.0.0", "react-native": "0.69.6", - "react-native-month": "^1.4.0", + "react-native-month": "^1.5.0", "react-native-web": "~0.18.9" }, "devDependencies": { diff --git a/example/tsconfig.json b/example/tsconfig.json index ee68629..ed35a48 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -6,7 +6,7 @@ "esnext" ], "paths": { - "react-native-agenda": [ + "agenda-react-native": [ "../src/index" ] }, diff --git a/example/yarn.lock b/example/yarn.lock index 2a5fb93..981dc0e 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -8617,10 +8617,10 @@ "resolved" "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz" "version" "0.0.7" -"react-native-month@^1.4.0": - "integrity" "sha512-fOtQ6CpsHkGEqRxw1E3SR/OqZskGeIgoIgZj+xorQDlMgATXxPac4sAWqjsB7TrDUz4yi4kGxKONQjYwG+k3fg==" - "resolved" "https://registry.npmjs.org/react-native-month/-/react-native-month-1.4.0.tgz" - "version" "1.4.0" +"react-native-month@^1.5.0": + "integrity" "sha512-pKJlueHhSZ2D9RnDshsAFkV28Z/IB1E+9/8nStMDJnS/UYfi/tsBbJO42xbKG4jOV0rymymnRD0xZQKOejG0eA==" + "resolved" "https://registry.npmjs.org/react-native-month/-/react-native-month-1.5.0.tgz" + "version" "1.5.0" "react-native-web@~0.18.9": "integrity" "sha512-BaV5Mpe7u9pN5vTRDW2g+MLh6PbPBJZpXRQM3Jr2cNv7hNa3sxCGh9T+NcW6wOFzf/+USrdrEPI1M9wNyr7vyA==" diff --git a/package-lock.json b/package-lock.json index 0ce9e1c..a02b79a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "dayjs": "^1.11.5", - "react-native-month": "^1.4.0" + "react-native-month": "^1.5.0" }, "devDependencies": { "@babel/core": "^7.19.1", @@ -14078,9 +14078,9 @@ "integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g==" }, "node_modules/react-native-month": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.4.0.tgz", - "integrity": "sha512-fOtQ6CpsHkGEqRxw1E3SR/OqZskGeIgoIgZj+xorQDlMgATXxPac4sAWqjsB7TrDUz4yi4kGxKONQjYwG+k3fg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.5.0.tgz", + "integrity": "sha512-pKJlueHhSZ2D9RnDshsAFkV28Z/IB1E+9/8nStMDJnS/UYfi/tsBbJO42xbKG4jOV0rymymnRD0xZQKOejG0eA==", "peerDependencies": { "react": "*", "react-native": "*" @@ -26647,9 +26647,9 @@ "integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g==" }, "react-native-month": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.4.0.tgz", - "integrity": "sha512-fOtQ6CpsHkGEqRxw1E3SR/OqZskGeIgoIgZj+xorQDlMgATXxPac4sAWqjsB7TrDUz4yi4kGxKONQjYwG+k3fg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-native-month/-/react-native-month-1.5.0.tgz", + "integrity": "sha512-pKJlueHhSZ2D9RnDshsAFkV28Z/IB1E+9/8nStMDJnS/UYfi/tsBbJO42xbKG4jOV0rymymnRD0xZQKOejG0eA==", "requires": {} }, "react-refresh": { diff --git a/package.json b/package.json index ff0b5ee..65b9029 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,6 @@ }, "dependencies": { "dayjs": "^1.11.5", - "react-native-month": "^1.4.0" + "react-native-month": "^1.5.0" } } diff --git a/src/components/Agenda/Agenda.tsx b/src/components/Agenda/Agenda.tsx index 695b939..2ba1bfb 100644 --- a/src/components/Agenda/Agenda.tsx +++ b/src/components/Agenda/Agenda.tsx @@ -7,6 +7,7 @@ import { Events } from '../Events/Events'; import { viewStyles } from './Agenda.styles'; import dayjs from 'dayjs'; +import { Week } from '../Week/Week'; export const Agenda = ({ selectedDay, @@ -17,6 +18,7 @@ export const Agenda = ({ renderSectionHeader, locale = 'en', firstDayMonday = false, + viewType = 'month', }: AgendaProps) => { const sectionListRef = useRef(null); const currentDay = useMemo(() => selectedDay ?? new Date(), [selectedDay]); @@ -24,7 +26,13 @@ export const Agenda = ({ const onDayPressCallback = useCallback( (date: Date) => { onDayPress?.(date); - const dayIndex = dayjs(date).date() - 1; + + const dayIndex = + viewType === 'week' + ? firstDayMonday + ? dayjs(date).isoWeekday() - 1 + : date.getDay() + : dayjs(date).date() - 1; sectionListRef.current?.scrollToLocation({ sectionIndex: dayIndex, @@ -32,31 +40,50 @@ export const Agenda = ({ viewPosition: 0, }); }, - [onDayPress] + [firstDayMonday, onDayPress, viewType] ); - const { markedDays } = useAgendaEvents(currentDay, events ?? []); + const { markedDays } = useAgendaEvents( + currentDay, + events ?? [], + viewType, + firstDayMonday + ); return ( - - + + + )} + {viewType === 'week' && ( + - + )} ); diff --git a/src/components/Events/Events.hooks.ts b/src/components/Events/Events.hooks.ts index c8f63c9..082ff80 100644 --- a/src/components/Events/Events.hooks.ts +++ b/src/components/Events/Events.hooks.ts @@ -1,20 +1,30 @@ import dayjs from 'dayjs'; import { useMemo } from 'react'; import { SectionListData } from 'react-native'; -import { Event, ExtendedMarkedDays } from 'src/types'; +import { AgendaProps, Event, ExtendedMarkedDays } from 'src/types'; export const useMonthEvents = ( currentDay: Date, - markedDays?: ExtendedMarkedDays + firstDayMonday: boolean, + markedDays?: ExtendedMarkedDays, + viewType?: AgendaProps['viewType'] ) => { + const eventDaysCount = + viewType === 'week' ? 7 : dayjs(currentDay).daysInMonth(); const monthDays = useMemo( - () => new Array(dayjs(currentDay).daysInMonth()).fill(true), - [currentDay] + () => new Array(eventDaysCount).fill(true), + [eventDaysCount] ); + const startWeekType = firstDayMonday ? 'isoWeek' : 'week'; + const weekStartDay = dayjs(currentDay).startOf(startWeekType); + const sections: SectionListData[] = useMemo(() => { return monthDays.map((_, index): SectionListData => { - const day = dayjs(currentDay).date(index + 1); + const day = + viewType === 'week' + ? weekStartDay.add(index, 'day') + : dayjs(currentDay).date(index + 1); const key = day.format('YYYY-MM-DD'); let data: Event[] = []; @@ -28,7 +38,7 @@ export const useMonthEvents = ( title: day.format('ddd, MMM D'), }; }); - }, [currentDay, markedDays, monthDays]); + }, [currentDay, markedDays, monthDays, viewType, weekStartDay]); return sections; }; diff --git a/src/components/Events/Events.tsx b/src/components/Events/Events.tsx index 107531b..fab7454 100644 --- a/src/components/Events/Events.tsx +++ b/src/components/Events/Events.tsx @@ -1,7 +1,12 @@ import dayjs from 'dayjs'; import React, { forwardRef, Ref, useCallback } from 'react'; import { SectionList, SectionListProps, Text, View } from 'react-native'; -import { Event as Event, ExtendedMarkedDays, ThemeType } from 'src/types'; +import { + AgendaProps, + Event as Event, + ExtendedMarkedDays, + ThemeType, +} from 'src/types'; import { EventItem } from '../EventItem/EventItem'; import { Line } from '../Line/Line'; import { useMonthEvents } from './Events.hooks'; @@ -14,6 +19,8 @@ type EventsProps = { onEventPress?: (event: Event) => void; renderSectionHeader?: SectionListProps['renderSectionHeader']; theme?: ThemeType; + viewType: AgendaProps['viewType']; + firstDayMonday: boolean; }; const keyExtractor = (item: Event, index: number) => `${item.name}-${index}`; @@ -26,10 +33,17 @@ export const Events = forwardRef( onEventPress, renderSectionHeader, theme, + viewType, + firstDayMonday, }: EventsProps, ref: Ref ) => { - const sections = useMonthEvents(currentDay, markedDays); + const sections = useMonthEvents( + currentDay, + firstDayMonday, + markedDays, + viewType + ); const renderEvent = useCallback( ({ item: event }) => { diff --git a/src/components/Week/Week.hooks.ts b/src/components/Week/Week.hooks.ts new file mode 100644 index 0000000..c989573 --- /dev/null +++ b/src/components/Week/Week.hooks.ts @@ -0,0 +1,35 @@ +import dayjs from 'dayjs'; +import { DayType } from 'react-native-month/lib/typescript/src/types'; + +export const useWeekDays = ( + selectedDate: Date, + firstDayMonday: boolean +): DayType[] => { + const days: DayType[] = []; + + const startWeekType = firstDayMonday ? 'isoWeek' : 'week'; + const startOfWeek = dayjs(selectedDate).startOf(startWeekType); + + for (let index = 0; index < 7; index++) { + const currentDay = startOfWeek.add(index, 'day'); + const dow = currentDay.day(); + const isActive = currentDay.isSame(selectedDate); + + days.push({ + key: currentDay.format('YYYY-MM-DD'), + id: currentDay.format('YYYY-MM-DD'), + date: currentDay.toDate(), + isToday: currentDay.isSame(dayjs()), + isWeekend: dow === 6 || dow === 0, + isMonthDate: true, + isActive, + isStartDate: isActive, + isEndDate: isActive, + isOutOfRange: false, + isVisible: true, + isHidden: false, + }); + } + + return days; +}; diff --git a/src/components/Week/Week.styles.ts b/src/components/Week/Week.styles.ts new file mode 100644 index 0000000..63682d1 --- /dev/null +++ b/src/components/Week/Week.styles.ts @@ -0,0 +1,9 @@ +import { StyleSheet } from 'react-native'; +import { spacing } from '../../constants/spacing'; + +export const viewStyles = StyleSheet.create({ + weekContainer: { + flexDirection: 'row', + marginBottom: spacing.small, + }, +}); diff --git a/src/components/Week/Week.tsx b/src/components/Week/Week.tsx new file mode 100644 index 0000000..c7609b9 --- /dev/null +++ b/src/components/Week/Week.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { View } from 'react-native'; +import { + LocaleType, + WeekDays, + Day, + getDayNames, + MarkedDays, +} from 'react-native-month'; +import { MonthThemeType } from 'src/types'; +import { useWeekDays } from './Week.hooks'; + +import { viewStyles } from './Week.styles'; + +type WeekProps = { + selectedDate: Date; + monthTheme?: MonthThemeType; + locale: LocaleType; + firstDayMonday: boolean; + markedDays?: MarkedDays; + onPress: (date: Date) => void; +}; + +export const Week = ({ + selectedDate, + monthTheme = {}, + locale, + firstDayMonday, + markedDays = {}, + onPress, +}: WeekProps) => { + const weekDayNames = getDayNames(locale, firstDayMonday); + const days = useWeekDays(selectedDate, firstDayMonday); + + return ( + + + + {days.map((day) => ( + + ))} + + + ); +}; diff --git a/src/hooks/use-agenda-events.ts b/src/hooks/use-agenda-events.ts index c1add89..f515d43 100644 --- a/src/hooks/use-agenda-events.ts +++ b/src/hooks/use-agenda-events.ts @@ -1,37 +1,43 @@ import dayjs from 'dayjs'; import { useMemo } from 'react'; -import { Event, ExtendedMarkedDays } from 'src/types'; - -export const useAgendaEvents = (currentMonth: Date, events: Event[]) => { +import { AgendaProps, Event, ExtendedMarkedDays } from 'src/types'; + +export const useAgendaEvents = ( + currentMonth: Date, + events: Event[], + viewType: AgendaProps['viewType'], + firstDayMonday: boolean +) => { return useMemo(() => { const markedDays: ExtendedMarkedDays = {}; - const monthStartingDay = dayjs(currentMonth) - .startOf('month') - .startOf('day'); - const monthEndingDay = dayjs(currentMonth).endOf('month').endOf('day'); + const rangeType = + viewType === 'week' ? (firstDayMonday ? 'isoWeek' : 'week') : 'month'; + const firstDay = dayjs(currentMonth).startOf(rangeType).startOf('day'); + + const endingDay = dayjs(currentMonth).endOf(rangeType).endOf('day'); events.forEach((event) => { if (dayjs(event.endDate).isBefore(dayjs(event.startDate))) { throw new Error(`${event.name} startDate must be previous to endDate`); } - if (dayjs(event.endDate).isBefore(monthStartingDay)) { + if (dayjs(event.endDate).isBefore(firstDay)) { // previous month event return; } - if (dayjs(event.startDate).isAfter(monthEndingDay)) { + if (dayjs(event.startDate).isAfter(endingDay)) { // future month event return; } - let currentDay = dayjs(event.startDate).isBefore(monthStartingDay) - ? monthStartingDay + let currentDay = dayjs(event.startDate).isBefore(firstDay) + ? firstDay : dayjs(event.startDate); - const lastDay = dayjs(event.endDate).isBefore(monthEndingDay) + const lastDay = dayjs(event.endDate).isBefore(endingDay) ? dayjs(event.endDate) - : monthEndingDay; + : endingDay; while (currentDay.isSameOrBefore(lastDay, 'day')) { const dot = { color: event.color, selectedColor: event.color }; @@ -52,24 +58,5 @@ export const useAgendaEvents = (currentMonth: Date, events: Event[]) => { }); return { markedDays }; - }, [currentMonth, events]); -}; - -export const getMonthEvents = (currentMonth: Date, events: Event[]) => { - return events.filter((event) => { - if (dayjs(event.endDate).isBefore(dayjs(event.startDate))) { - throw new Error(`${event.name} startDate must be previous to endDate`); - } - - const monthStartingDay = dayjs(currentMonth) - .startOf('month') - .startOf('day'); - const monthEndingDay = dayjs(currentMonth).endOf('month').endOf('day'); - - if (dayjs(event.startDate).isBetween(monthStartingDay, monthEndingDay)) { - return true; - } - - return dayjs(event.endDate).isBetween(monthStartingDay, monthEndingDay); - }); + }, [currentMonth, events, firstDayMonday, viewType]); }; diff --git a/src/index.tsx b/src/index.tsx index 2ce15bf..8a7b168 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,11 @@ import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; +import isoWeek from 'dayjs/plugin/isoWeek'; dayjs.extend(isBetween); dayjs.extend(isSameOrBefore); +dayjs.extend(isoWeek); export * from './components/Agenda/Agenda'; export * from './types'; diff --git a/src/types.ts b/src/types.ts index b8c6f76..65f3dcc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,6 +46,8 @@ interface AgendaMonthProps 'firstDayMonday' | 'dayNames' | 'disableOffsetDays' > {} +type AgendaViewType = 'month' | 'week' | 'none'; + export interface AgendaProps extends AgendaMonthProps { /** * selected day of the Agenda @@ -63,4 +65,5 @@ export interface AgendaProps extends AgendaMonthProps { renderSectionHeader?: SectionListProps['renderSectionHeader']; locale?: LocaleType; firstDayMonday?: boolean; + viewType?: AgendaViewType; } diff --git a/yarn.lock b/yarn.lock index cc567c2..6224c38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6071,10 +6071,10 @@ "resolved" "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz" "version" "0.0.7" -"react-native-month@^1.4.0": - "integrity" "sha512-fOtQ6CpsHkGEqRxw1E3SR/OqZskGeIgoIgZj+xorQDlMgATXxPac4sAWqjsB7TrDUz4yi4kGxKONQjYwG+k3fg==" - "resolved" "https://registry.npmjs.org/react-native-month/-/react-native-month-1.4.0.tgz" - "version" "1.4.0" +"react-native-month@^1.5.0": + "integrity" "sha512-pKJlueHhSZ2D9RnDshsAFkV28Z/IB1E+9/8nStMDJnS/UYfi/tsBbJO42xbKG4jOV0rymymnRD0xZQKOejG0eA==" + "resolved" "https://registry.npmjs.org/react-native-month/-/react-native-month-1.5.0.tgz" + "version" "1.5.0" "react-native@*", "react-native@^0.69.6", "react-native@>=0.59": "integrity" "sha512-wwXpqM+12kdEYdBZCJUb5SBu95CzgejrwFeYJ78RzHZV/Sj6DBRekbsHGrDDsY4R25QXALQxy4DQYQCObVvWjA=="