@umituz/react-native-calendar
Version:
Generic calendar component for React Native apps with timezone support, event management, and system calendar sync
308 lines (277 loc) • 8.79 kB
text/typescript
/**
* Calendar Store (Zustand)
*
* Global state management for calendar functionality.
* Manages calendar view state, selected date, and events.
*
* Design Philosophy:
* - Zustand for lightweight state
* - AsyncStorage for persistence
* - Generic event handling
* - Timezone-aware via CalendarService
*/
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { CalendarEvent, CreateCalendarEventRequest, UpdateCalendarEventRequest } from '../../domain/entities/CalendarEvent.entity';
import { CalendarService } from '../services/CalendarService';
/**
* Calendar view mode
*/
export type CalendarViewMode = 'month' | 'week' | 'day' | 'list';
/**
* Calendar state interface
*/
interface CalendarState {
// State
events: CalendarEvent[];
selectedDate: Date;
currentMonth: Date;
viewMode: CalendarViewMode;
isLoading: boolean;
error: string | null;
// Actions
actions: {
// Event CRUD
loadEvents: () => Promise<void>;
addEvent: (request: CreateCalendarEventRequest) => Promise<void>;
updateEvent: (request: UpdateCalendarEventRequest) => Promise<void>;
deleteEvent: (id: string) => Promise<void>;
completeEvent: (id: string) => Promise<void>;
uncompleteEvent: (id: string) => Promise<void>;
// Navigation
setSelectedDate: (date: Date) => void;
goToToday: () => void;
navigateMonth: (direction: 'prev' | 'next') => void;
navigateWeek: (direction: 'prev' | 'next') => void;
setCurrentMonth: (date: Date) => void;
// View mode
setViewMode: (mode: CalendarViewMode) => void;
// Utilities
getEventsForDate: (date: Date) => CalendarEvent[];
getEventsForMonth: (year: number, month: number) => CalendarEvent[];
clearError: () => void;
clearAllEvents: () => Promise<void>;
};
}
/**
* Storage key for calendar events
*/
const STORAGE_KEY = 'calendar_events';
/**
* Generate unique ID for events
*/
const generateId = (): string => {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
/**
* Calendar Store
*/
export const useCalendarStore = create<CalendarState>()(
persist(
(set, get) => ({
// Initial State
events: [],
selectedDate: new Date(),
currentMonth: new Date(),
viewMode: 'month',
isLoading: false,
error: null,
// Actions
actions: {
/**
* Load events from storage
*/
loadEvents: async () => {
set({ isLoading: true, error: null });
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) {
const events = JSON.parse(stored);
// Restore Date objects
events.forEach((event: CalendarEvent) => {
event.createdAt = new Date(event.createdAt);
event.updatedAt = new Date(event.updatedAt);
});
set({ events, isLoading: false });
} else {
set({ isLoading: false });
}
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to load events',
isLoading: false
});
}
},
/**
* Add a new event
*/
addEvent: async (request: CreateCalendarEventRequest) => {
set({ isLoading: true, error: null });
try {
const newEvent: CalendarEvent = {
id: generateId(),
...request,
isCompleted: false,
createdAt: new Date(),
updatedAt: new Date(),
};
const events = [...get().events, newEvent];
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
set({ events, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to add event',
isLoading: false
});
}
},
/**
* Update an existing event
*/
updateEvent: async (request: UpdateCalendarEventRequest) => {
set({ isLoading: true, error: null });
try {
const events = get().events.map(event => {
if (event.id === request.id) {
return {
...event,
...request,
updatedAt: new Date(),
};
}
return event;
});
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
set({ events, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to update event',
isLoading: false
});
}
},
/**
* Delete an event
*/
deleteEvent: async (id: string) => {
set({ isLoading: true, error: null });
try {
const events = get().events.filter(event => event.id !== id);
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
set({ events, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to delete event',
isLoading: false
});
}
},
/**
* Mark event as completed
*/
completeEvent: async (id: string) => {
await get().actions.updateEvent({ id, isCompleted: true });
},
/**
* Mark event as incomplete
*/
uncompleteEvent: async (id: string) => {
await get().actions.updateEvent({ id, isCompleted: false });
},
/**
* Set selected date
*/
setSelectedDate: (date: Date) => {
set({ selectedDate: date });
},
/**
* Go to today's date
*/
goToToday: () => {
const today = new Date();
set({
selectedDate: today,
currentMonth: today,
});
},
/**
* Navigate to previous/next month
*/
navigateMonth: (direction: 'prev' | 'next') => {
const currentMonth = get().currentMonth;
const newMonth = direction === 'prev'
? CalendarService.getPreviousMonth(currentMonth)
: CalendarService.getNextMonth(currentMonth);
set({ currentMonth: newMonth });
},
/**
* Navigate to previous/next week
*/
navigateWeek: (direction: 'prev' | 'next') => {
const selectedDate = get().selectedDate;
const newDate = direction === 'prev'
? CalendarService.getPreviousWeek(selectedDate)
: CalendarService.getNextWeek(selectedDate);
set({ selectedDate: newDate });
},
/**
* Set current month directly
*/
setCurrentMonth: (date: Date) => {
set({ currentMonth: date });
},
/**
* Set view mode
*/
setViewMode: (mode: CalendarViewMode) => {
set({ viewMode: mode });
},
/**
* Get events for a specific date
*/
getEventsForDate: (date: Date) => {
const events = get().events;
return CalendarService.getEventsForDate(date, events);
},
/**
* Get events for a specific month
*/
getEventsForMonth: (year: number, month: number) => {
const events = get().events;
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
return CalendarService.getEventsInRange(firstDay, lastDay, events);
},
/**
* Clear error state
*/
clearError: () => {
set({ error: null });
},
/**
* Clear all events (for testing/reset)
*/
clearAllEvents: async () => {
set({ isLoading: true, error: null });
try {
await AsyncStorage.removeItem(STORAGE_KEY);
set({ events: [], isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to clear events',
isLoading: false
});
}
},
},
}),
{
name: 'calendar-storage',
storage: createJSONStorage(() => AsyncStorage),
// Only persist events, not UI state
partialize: (state) => ({ events: state.events }),
}
)
);