react-native-bigger-calendar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "react-native-bigger-calendar",
3
+ "version": "0.1.0",
4
+ "description": "A generic, themeable month/week/day calendar for React Native with pinch-to-zoom, virtualized paging, and a render-prop event API.",
5
+ "keywords": [
6
+ "react-native",
7
+ "calendar",
8
+ "timetable",
9
+ "agenda",
10
+ "month-view",
11
+ "week-view",
12
+ "day-view",
13
+ "reanimated",
14
+ "expo"
15
+ ],
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/afonsojramos/react-native-bigger-calendar.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/afonsojramos/react-native-bigger-calendar/issues"
23
+ },
24
+ "homepage": "https://github.com/afonsojramos/react-native-bigger-calendar#readme",
25
+ "sideEffects": false,
26
+ "source": "./src/index.tsx",
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.mjs",
29
+ "react-native": "./src/index.tsx",
30
+ "types": "./dist/index.d.ts",
31
+ "files": [
32
+ "src",
33
+ "dist",
34
+ "README.md",
35
+ "!**/__tests__",
36
+ "!**/__mocks__"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsdown",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "jest",
42
+ "clean": "rm -rf dist",
43
+ "prepublishOnly": "npm run build"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "peerDependencies": {
49
+ "@legendapp/list": ">=3",
50
+ "date-fns": ">=3",
51
+ "react": ">=18",
52
+ "react-native": ">=0.74",
53
+ "react-native-gesture-handler": ">=2.16",
54
+ "react-native-reanimated": ">=4.0.0 <5",
55
+ "react-native-worklets": ">=0.5"
56
+ },
57
+ "devDependencies": {
58
+ "@babel/core": "^7.25.0",
59
+ "@babel/preset-env": "^7.25.0",
60
+ "@babel/preset-react": "^7.25.0",
61
+ "@babel/preset-typescript": "^7.25.0",
62
+ "@legendapp/list": "^3.0.6",
63
+ "@types/jest": "^29.5.0",
64
+ "@types/react": "^19.2.17",
65
+ "babel-jest": "^29.7.0",
66
+ "date-fns": "4.4.0",
67
+ "jest": "^29.7.0",
68
+ "react": "19.2.3",
69
+ "react-native": "0.85.3",
70
+ "react-native-gesture-handler": "~2.31.1",
71
+ "react-native-reanimated": "4.3.1",
72
+ "tsdown": "^0.22.3",
73
+ "typescript": "^5.5.0"
74
+ }
75
+ }
@@ -0,0 +1,147 @@
1
+ import { useMemo } from 'react';
2
+ import { useSharedValue } from 'react-native-reanimated';
3
+ import { CalendarThemeProvider, mergeTheme, type PartialCalendarTheme } from '../theme';
4
+ import type {
5
+ CalendarEvent,
6
+ CalendarMode,
7
+ EventKeyExtractor,
8
+ RenderEvent,
9
+ WeekStartsOn,
10
+ } from '../types';
11
+ import { DefaultEvent } from './DefaultEvent';
12
+ import { MonthPager } from './MonthPager';
13
+ import { DEFAULT_HOUR_HEIGHT, TimeGrid } from './TimeGrid';
14
+
15
+ export type CalendarProps<T> = {
16
+ events: CalendarEvent<T>[];
17
+ mode: CalendarMode;
18
+ date: Date;
19
+ onChangeDate: (date: Date) => void;
20
+ onPressEvent: (event: CalendarEvent<T>) => void;
21
+ /** Tap a day cell (month mode) — e.g. drill into the day view. */
22
+ onPressDay?: (date: Date) => void;
23
+ /** Tap the "+N more" overflow label in a month cell. */
24
+ onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
25
+ /** Tap empty space on the week/day grid; receives the date+time pressed. */
26
+ onPressCell?: (date: Date) => void;
27
+ /** Max events shown per month cell before they collapse into "+N more". */
28
+ maxVisibleEventCount?: number;
29
+ /** First day of the week. Sunday = 0 (default) … Saturday = 6. */
30
+ weekStartsOn?: WeekStartsOn;
31
+ /** Replace the built-in event box. Return a `flex: 1` element. */
32
+ renderEvent?: RenderEvent<T>;
33
+ /** Stable key per event. Defaults to start-time + index. */
34
+ keyExtractor?: EventKeyExtractor<T>;
35
+ /** Partial theme merged over the defaults. */
36
+ theme?: PartialCalendarTheme;
37
+ /** Externally-owned per-hour row height (week/day). Created internally if omitted. */
38
+ cellHeight?: ReturnType<typeof useSharedValue<number>>;
39
+ /** Initial per-hour row height in px (week/day). Default 64. */
40
+ hourHeight?: number;
41
+ minHourHeight?: number;
42
+ maxHourHeight?: number;
43
+ hourColumnWidth?: number;
44
+ /** First hour shown on the week/day grid (0–23). Default 0. */
45
+ minHour?: number;
46
+ /** Last hour shown on the week/day grid, exclusive (1–24). Default 24. */
47
+ maxHour?: number;
48
+ /** Show hour labels in 12-hour AM/PM form. Default false (24h). */
49
+ ampm?: boolean;
50
+ /** Initial vertical scroll, in minutes from midnight (week/day). */
51
+ scrollOffsetMinutes?: number;
52
+ /** Show the current-time line on the week/day grid. Default true. */
53
+ showNowIndicator?: boolean;
54
+ /** BCP-47 locale for weekday labels. Defaults to the device locale. */
55
+ locale?: string;
56
+ /**
57
+ * Allow a fling to carry across several pages before snapping. Default false:
58
+ * one day/week/month per swipe.
59
+ */
60
+ freeSwipe?: boolean;
61
+ /** Custom header above the week/day grid. Receives the visible days. */
62
+ renderTimeGridHeader?: (days: Date[]) => React.ReactNode;
63
+ };
64
+
65
+ // Derive a key purely from event data so identity is stable across reorders and
66
+ // list mutations. Supply your own `keyExtractor` returning a real id when events
67
+ // can share an identical start/end/title.
68
+ const defaultKeyExtractor: EventKeyExtractor<unknown> = (event) =>
69
+ `${event.start.toISOString()}|${event.end.toISOString()}|${event.title ?? ''}`;
70
+
71
+ export function Calendar<T>({
72
+ events,
73
+ mode,
74
+ date,
75
+ onChangeDate,
76
+ onPressEvent,
77
+ onPressDay,
78
+ onPressMore,
79
+ onPressCell,
80
+ maxVisibleEventCount = 2,
81
+ weekStartsOn = 0,
82
+ renderEvent = DefaultEvent,
83
+ keyExtractor = defaultKeyExtractor as EventKeyExtractor<T>,
84
+ theme,
85
+ cellHeight: cellHeightProp,
86
+ hourHeight = DEFAULT_HOUR_HEIGHT,
87
+ minHourHeight,
88
+ maxHourHeight,
89
+ hourColumnWidth,
90
+ minHour,
91
+ maxHour,
92
+ ampm,
93
+ scrollOffsetMinutes,
94
+ showNowIndicator,
95
+ locale,
96
+ freeSwipe,
97
+ renderTimeGridHeader,
98
+ }: CalendarProps<T>) {
99
+ const mergedTheme = useMemo(() => mergeTheme(theme), [theme]);
100
+ const internalCellHeight = useSharedValue(hourHeight);
101
+ const cellHeight = cellHeightProp ?? internalCellHeight;
102
+
103
+ return (
104
+ <CalendarThemeProvider value={mergedTheme}>
105
+ {mode === 'month' ? (
106
+ <MonthPager
107
+ date={date}
108
+ events={events}
109
+ maxVisibleEventCount={maxVisibleEventCount}
110
+ weekStartsOn={weekStartsOn}
111
+ renderEvent={renderEvent}
112
+ keyExtractor={keyExtractor}
113
+ onPressDay={onPressDay}
114
+ onPressEvent={onPressEvent}
115
+ onPressMore={onPressMore}
116
+ onChangeDate={onChangeDate}
117
+ freeSwipe={freeSwipe}
118
+ />
119
+ ) : (
120
+ <TimeGrid
121
+ mode={mode}
122
+ date={date}
123
+ events={events}
124
+ cellHeight={cellHeight}
125
+ hourHeight={hourHeight}
126
+ weekStartsOn={weekStartsOn}
127
+ renderEvent={renderEvent}
128
+ keyExtractor={keyExtractor}
129
+ scrollOffsetMinutes={scrollOffsetMinutes}
130
+ hourColumnWidth={hourColumnWidth}
131
+ minHour={minHour}
132
+ maxHour={maxHour}
133
+ ampm={ampm}
134
+ minHourHeight={minHourHeight}
135
+ maxHourHeight={maxHourHeight}
136
+ showNowIndicator={showNowIndicator}
137
+ locale={locale}
138
+ freeSwipe={freeSwipe}
139
+ onPressEvent={onPressEvent}
140
+ onPressCell={onPressCell}
141
+ onChangeDate={onChangeDate}
142
+ renderHeader={renderTimeGridHeader}
143
+ />
144
+ )}
145
+ </CalendarThemeProvider>
146
+ );
147
+ }
@@ -0,0 +1,57 @@
1
+ import { format } from 'date-fns';
2
+ import { StyleSheet, Text, TouchableOpacity } from 'react-native';
3
+ import { useCalendarTheme } from '../theme';
4
+ import type { RenderEventArgs } from '../types';
5
+
6
+ /**
7
+ * The built-in event renderer: a filled, rounded box showing the event title
8
+ * and (on the day/week grid) its time range. Pass your own `renderEvent` to
9
+ * `<Calendar>` to replace it entirely.
10
+ */
11
+ export function DefaultEvent<T>({ event, mode, onPress }: RenderEventArgs<T>) {
12
+ const theme = useCalendarTheme();
13
+ const showTime = mode !== 'month';
14
+
15
+ return (
16
+ <TouchableOpacity
17
+ style={[styles.box, { backgroundColor: theme.colors.eventBackground }]}
18
+ onPress={onPress}
19
+ activeOpacity={0.7}
20
+ accessibilityRole="button"
21
+ accessibilityLabel={event.title}
22
+ >
23
+ {event.title ? (
24
+ <Text
25
+ style={[theme.text.eventTitle, { color: theme.colors.eventText }]}
26
+ numberOfLines={mode === 'day' ? undefined : 1}
27
+ ellipsizeMode="tail"
28
+ allowFontScaling={false}
29
+ >
30
+ {event.title}
31
+ </Text>
32
+ ) : null}
33
+ {showTime ? (
34
+ <Text
35
+ style={[styles.time, { color: theme.colors.eventText }]}
36
+ numberOfLines={1}
37
+ allowFontScaling={false}
38
+ >
39
+ {`${format(event.start, 'HH:mm')} - ${format(event.end, 'HH:mm')}`}
40
+ </Text>
41
+ ) : null}
42
+ </TouchableOpacity>
43
+ );
44
+ }
45
+
46
+ const styles = StyleSheet.create({
47
+ box: {
48
+ flex: 1,
49
+ borderRadius: 6,
50
+ paddingVertical: 2,
51
+ paddingHorizontal: 4,
52
+ overflow: 'hidden',
53
+ },
54
+ time: {
55
+ fontSize: 11,
56
+ },
57
+ });
@@ -0,0 +1,165 @@
1
+ import {
2
+ LegendList,
3
+ type LegendListRef,
4
+ type LegendListRenderItemProps,
5
+ type OnViewableItemsChangedInfo,
6
+ } from '@legendapp/list/react-native';
7
+ import { addMonths, differenceInCalendarMonths, startOfMonth } from 'date-fns';
8
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
+ import { StyleSheet, useWindowDimensions, View } from 'react-native';
10
+ import type { CalendarEvent, EventKeyExtractor, RenderEvent, WeekStartsOn } from '../types';
11
+ import { MonthView } from './MonthView';
12
+
13
+ // Months rendered either side of the current page. LegendList virtualises, so
14
+ // only a few mount at once; a wide window (5 years each way) means the user
15
+ // effectively never runs out of months to swipe. Items are keyed by month and
16
+ // never recycled.
17
+ const PAGE_WINDOW = 60;
18
+ // A page must be ~fully on screen before it becomes the committed month, so
19
+ // paging commits once per settle rather than mid-swipe.
20
+ const PAGE_VIEWABILITY = { itemVisiblePercentThreshold: 90 };
21
+
22
+ export type MonthPagerProps<T> = {
23
+ date: Date;
24
+ events: CalendarEvent<T>[];
25
+ maxVisibleEventCount: number;
26
+ weekStartsOn: WeekStartsOn;
27
+ renderEvent: RenderEvent<T>;
28
+ keyExtractor: EventKeyExtractor<T>;
29
+ onPressDay?: (date: Date) => void;
30
+ onPressEvent: (event: CalendarEvent<T>) => void;
31
+ onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
32
+ onChangeDate: (date: Date) => void;
33
+ freeSwipe?: boolean;
34
+ };
35
+
36
+ function MonthPagerInner<T>({
37
+ date,
38
+ events,
39
+ maxVisibleEventCount,
40
+ weekStartsOn,
41
+ renderEvent,
42
+ keyExtractor,
43
+ onPressDay,
44
+ onPressEvent,
45
+ onPressMore,
46
+ onChangeDate,
47
+ freeSwipe = false,
48
+ }: MonthPagerProps<T>) {
49
+ const { width, height } = useWindowDimensions();
50
+ const listRef = useRef<LegendListRef>(null);
51
+ // Horizontal list items need an explicit cross-axis height; seed it with the
52
+ // window height (so it renders immediately and in tests) and refine to the
53
+ // exact area on layout. Without this the grid collapses to 0px.
54
+ const [pageHeight, setPageHeight] = useState(height);
55
+
56
+ // A fixed window of months, anchored once and aligned to the month start. The
57
+ // array never shifts as the date changes, so paging never re-renders a page's
58
+ // content — LegendList virtualises and keys by month.
59
+ const [anchorDate] = useState(date);
60
+ const anchor = useMemo(() => startOfMonth(anchorDate), [anchorDate]);
61
+ const monthDates = useMemo(
62
+ () => Array.from({ length: PAGE_WINDOW * 2 + 1 }, (_, i) => addMonths(anchor, i - PAGE_WINDOW)),
63
+ [anchor],
64
+ );
65
+ const indexOfMonth = useCallback(
66
+ (target: Date) => differenceInCalendarMonths(startOfMonth(target), anchor) + PAGE_WINDOW,
67
+ [anchor],
68
+ );
69
+
70
+ // The committed month's page is the centred/active one. Derived (not stored)
71
+ // so it always reflects the date. `viewedIndexRef` tracks where the list
72
+ // actually sits, letting us tell swipe-driven month changes from external ones.
73
+ const activeIndex = indexOfMonth(date);
74
+ const viewedIndexRef = useRef(activeIndex);
75
+
76
+ const handleViewableItemsChanged = useCallback(
77
+ (info: OnViewableItemsChangedInfo<Date>) => {
78
+ const settled = info.viewableItems.find((token) => token.isViewable);
79
+ if (settled?.index == null || settled.index === viewedIndexRef.current) return;
80
+ viewedIndexRef.current = settled.index;
81
+ if (settled.item) onChangeDate(settled.item);
82
+ },
83
+ [onChangeDate],
84
+ );
85
+
86
+ // Realign the list when the month changes from outside a swipe (e.g. a "today"
87
+ // button). Swipe-driven changes already match.
88
+ useEffect(() => {
89
+ if (activeIndex === viewedIndexRef.current) return;
90
+ viewedIndexRef.current = activeIndex;
91
+ listRef.current?.scrollToIndex({ index: activeIndex, animated: false });
92
+ }, [activeIndex]);
93
+
94
+ const snapToIndices = useMemo(() => monthDates.map((_, index) => index), [monthDates]);
95
+ const keyExtractorList = useCallback((item: Date) => item.toISOString(), []);
96
+ const getFixedItemSize = useCallback(() => width, [width]);
97
+ const renderItem = useCallback(
98
+ ({ item }: LegendListRenderItemProps<Date>) => (
99
+ <View style={{ width, height: pageHeight }}>
100
+ <MonthView
101
+ date={item}
102
+ events={events}
103
+ maxVisibleEventCount={maxVisibleEventCount}
104
+ weekStartsOn={weekStartsOn}
105
+ renderEvent={renderEvent}
106
+ keyExtractor={keyExtractor}
107
+ onPressDay={onPressDay}
108
+ onPressEvent={onPressEvent}
109
+ onPressMore={onPressMore}
110
+ />
111
+ </View>
112
+ ),
113
+ [
114
+ width,
115
+ pageHeight,
116
+ events,
117
+ maxVisibleEventCount,
118
+ weekStartsOn,
119
+ renderEvent,
120
+ keyExtractor,
121
+ onPressDay,
122
+ onPressEvent,
123
+ onPressMore,
124
+ ],
125
+ );
126
+
127
+ return (
128
+ <View style={styles.pager} onLayout={(event) => setPageHeight(event.nativeEvent.layout.height)}>
129
+ <LegendList
130
+ // Remount when the measured page height changes so the list adopts the
131
+ // corrected item height. Without this the list can keep the oversized
132
+ // initial (window-height) seed and clip the last week row.
133
+ key={pageHeight}
134
+ ref={listRef}
135
+ style={styles.pagerList}
136
+ data={monthDates}
137
+ horizontal
138
+ recycleItems={false}
139
+ keyExtractor={keyExtractorList}
140
+ getFixedItemSize={getFixedItemSize}
141
+ // Default: native paging — each page is the viewport width, so a swipe
142
+ // hard-stops at the adjacent month and can't fling past it. With
143
+ // `freeSwipe`, momentum carries across months and snaps to a boundary.
144
+ pagingEnabled={!freeSwipe}
145
+ snapToIndices={freeSwipe ? snapToIndices : undefined}
146
+ initialScrollIndex={activeIndex}
147
+ showsHorizontalScrollIndicator={false}
148
+ viewabilityConfig={PAGE_VIEWABILITY}
149
+ onViewableItemsChanged={handleViewableItemsChanged}
150
+ renderItem={renderItem}
151
+ />
152
+ </View>
153
+ );
154
+ }
155
+
156
+ export const MonthPager = memo(MonthPagerInner) as typeof MonthPagerInner;
157
+
158
+ const styles = StyleSheet.create({
159
+ pager: {
160
+ flex: 1,
161
+ },
162
+ pagerList: {
163
+ flex: 1,
164
+ },
165
+ });
@@ -0,0 +1,178 @@
1
+ import {
2
+ eachDayOfInterval,
3
+ endOfMonth,
4
+ endOfWeek,
5
+ format,
6
+ isSameMonth,
7
+ startOfDay,
8
+ startOfMonth,
9
+ startOfWeek,
10
+ } from 'date-fns';
11
+ import { memo, useMemo } from 'react';
12
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
13
+ import { useCalendarTheme } from '../theme';
14
+ import type { CalendarEvent, EventKeyExtractor, RenderEvent, WeekStartsOn } from '../types';
15
+ import { getIsToday, isWeekend } from '../utils/dates';
16
+ import { eventDayKeys } from '../utils/layout';
17
+
18
+ const chunkIntoWeeks = (days: Date[]): Date[][] => {
19
+ const weeks: Date[][] = [];
20
+ for (let index = 0; index < days.length; index += 7) {
21
+ weeks.push(days.slice(index, index + 7));
22
+ }
23
+ return weeks;
24
+ };
25
+
26
+ export type MonthViewProps<T> = {
27
+ date: Date;
28
+ events: CalendarEvent<T>[];
29
+ maxVisibleEventCount: number;
30
+ weekStartsOn: WeekStartsOn;
31
+ renderEvent: RenderEvent<T>;
32
+ keyExtractor: EventKeyExtractor<T>;
33
+ onPressDay?: (date: Date) => void;
34
+ onPressEvent: (event: CalendarEvent<T>) => void;
35
+ onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
36
+ };
37
+
38
+ function MonthViewInner<T>({
39
+ date,
40
+ events,
41
+ maxVisibleEventCount,
42
+ weekStartsOn,
43
+ renderEvent,
44
+ keyExtractor,
45
+ onPressDay,
46
+ onPressEvent,
47
+ onPressMore,
48
+ }: MonthViewProps<T>) {
49
+ const theme = useCalendarTheme();
50
+ const RenderEventComponent = renderEvent;
51
+
52
+ const weeks = useMemo(() => {
53
+ const start = startOfWeek(startOfMonth(date), { weekStartsOn });
54
+ const end = endOfWeek(endOfMonth(date), { weekStartsOn });
55
+ return chunkIntoWeeks(eachDayOfInterval({ start, end }));
56
+ }, [date, weekStartsOn]);
57
+
58
+ // Group events by calendar day once per `events` change, rather than scanning
59
+ // the whole list inside every one of the (up to) 42 day cells on each render.
60
+ // Multi-day events are indexed under every day they span.
61
+ const eventsByDay = useMemo(() => {
62
+ const map = new Map<string, CalendarEvent<T>[]>();
63
+ for (const event of events) {
64
+ for (const key of eventDayKeys(event)) {
65
+ const existing = map.get(key);
66
+ if (existing) existing.push(event);
67
+ else map.set(key, [event]);
68
+ }
69
+ }
70
+ return map;
71
+ }, [events]);
72
+
73
+ const renderDay = (day: Date) => {
74
+ const dayEvents = eventsByDay.get(startOfDay(day).toISOString()) ?? [];
75
+ const isCurrentMonth = isSameMonth(day, date);
76
+ const isToday = getIsToday(day);
77
+ const hiddenCount = dayEvents.length - maxVisibleEventCount;
78
+
79
+ const dateColor = isToday
80
+ ? theme.colors.todayText
81
+ : isCurrentMonth
82
+ ? theme.colors.text
83
+ : theme.colors.textDisabled;
84
+
85
+ // Summarise the cell for screen readers: full date, today marker, and how
86
+ // many events it holds (the chips inside are grouped under this cell).
87
+ const eventCount = dayEvents.length;
88
+ const accessibilityLabel = `${format(day, 'EEEE, d LLLL yyyy')}${isToday ? ', today' : ''}, ${eventCount} ${eventCount === 1 ? 'event' : 'events'}`;
89
+
90
+ return (
91
+ <TouchableOpacity
92
+ key={day.toISOString()}
93
+ style={[
94
+ styles.dayCell,
95
+ { borderColor: theme.colors.gridLine },
96
+ isWeekend(day) && { backgroundColor: theme.colors.weekendBackground },
97
+ ]}
98
+ onPress={onPressDay ? () => onPressDay(day) : undefined}
99
+ disabled={!onPressDay}
100
+ accessibilityRole={onPressDay ? 'button' : undefined}
101
+ accessibilityLabel={accessibilityLabel}
102
+ >
103
+ <View
104
+ style={[
105
+ styles.dateBadge,
106
+ isToday && {
107
+ backgroundColor: theme.colors.todayBackground,
108
+ borderRadius: theme.todayBadgeRadius,
109
+ },
110
+ ]}
111
+ >
112
+ <Text style={[theme.text.dateCell, { color: dateColor }]} allowFontScaling={false}>
113
+ {format(day, 'd')}
114
+ </Text>
115
+ </View>
116
+ {dayEvents.slice(0, maxVisibleEventCount).map((event, index) => (
117
+ <View key={keyExtractor(event, index)} style={styles.monthEvent}>
118
+ <RenderEventComponent event={event} mode="month" onPress={() => onPressEvent(event)} />
119
+ </View>
120
+ ))}
121
+ {hiddenCount > 0 ? (
122
+ <Text
123
+ style={[theme.text.more, styles.moreLabel, { color: theme.colors.textMuted }]}
124
+ onPress={onPressMore ? () => onPressMore(dayEvents, day) : undefined}
125
+ accessibilityRole="button"
126
+ accessibilityLabel={`Show ${hiddenCount} more events`}
127
+ allowFontScaling={false}
128
+ >
129
+ {`${hiddenCount} More`}
130
+ </Text>
131
+ ) : null}
132
+ </TouchableOpacity>
133
+ );
134
+ };
135
+
136
+ return (
137
+ <View style={styles.container}>
138
+ {weeks.map((week) => (
139
+ <View style={styles.weekRow} key={week[0].toISOString()}>
140
+ {week.map((day) => renderDay(day))}
141
+ </View>
142
+ ))}
143
+ </View>
144
+ );
145
+ }
146
+
147
+ export const MonthView = memo(MonthViewInner) as typeof MonthViewInner;
148
+
149
+ const styles = StyleSheet.create({
150
+ container: {
151
+ flex: 1,
152
+ },
153
+ weekRow: {
154
+ flex: 1,
155
+ flexDirection: 'row',
156
+ },
157
+ dayCell: {
158
+ flex: 1,
159
+ alignItems: 'center',
160
+ paddingTop: 4,
161
+ gap: 2,
162
+ overflow: 'hidden',
163
+ borderTopWidth: StyleSheet.hairlineWidth,
164
+ borderRightWidth: StyleSheet.hairlineWidth,
165
+ },
166
+ dateBadge: {
167
+ justifyContent: 'center',
168
+ alignItems: 'center',
169
+ height: 24,
170
+ width: 24,
171
+ },
172
+ monthEvent: {
173
+ width: '92%',
174
+ },
175
+ moreLabel: {
176
+ marginTop: 2,
177
+ },
178
+ });