🚀 Calendar Component - Installation Guide
Panduan lengkap untuk mengimplementasikan Calendar Component ke project baru.
📋 Prerequisites
Pastikan project Anda memiliki:
- ✅ React 18 atau lebih baru
- ✅ TypeScript 4.5+ (recommended)
- ✅ Node.js 18+ & npm/yarn
📦 Step 1: Install Dependencies
Required Dependencies
npm install react-big-calendar date-fns dayjs @tanstack/react-query
# atau
yarn add react-big-calendar date-fns dayjs @tanstack/react-query
Optional Dependencies (Untuk UI Enhancement)
npm install @mantine/core @mantine/hooks @mantine/notifications @tabler/icons-react
# atau
yarn add @mantine/core @mantine/hooks @mantine/notifications @tabler/icons-react
Mantine UI diperlukan untuk CustomToolbar dan Tooltip. Jika tidak menggunakan Mantine, Anda perlu modifikasi CustomEvent.tsx dan CustomToolbar.tsx.
📁 Step 2: Copy Files
Structure
Copy folder Calendar ke project Anda:
your-project/
└── src/
└── components/
└── Calendar/
├── index.tsx # Legacy wrapper (optional)
├── GenericCalendar.tsx # Main component ⭐
├── CustomEvent.tsx # Event renderer
├── CustomToolbar.tsx # Toolbar component
├── Calendar.module.css # Styles
├── types.ts # Type definitions
└── utils/
└── helper.ts # Helper functions
Files to Copy
- GenericCalendar.tsx - Component utama
- types.ts - Type definitions
- CustomEvent.tsx - Custom event renderer
- CustomToolbar.tsx - Custom toolbar
- Calendar.module.css - Styling
- utils/helper.ts - Helper functions
🔧 Step 3: Setup React Query
App-level Setup
Wrap aplikasi dengan QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30000, // 30 seconds
refetchOnWindowFocus: false,
retry: 1,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
<YourRoutes />
{/* Optional: DevTools untuk development */}
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
);
}
🎨 Step 4: Import Styles
Import React Big Calendar CSS
// Import RBC default styles
import 'react-big-calendar/lib/css/react-big-calendar.css';
Optional: Custom SCSS/CSS
Jika ingin override RBC styles:
// Override RBC default colors
.rbc-calendar {
font-family: 'Inter', sans-serif;
}
.rbc-today {
background-color: #e3f2fd;
}
.rbc-event {
border-radius: 6px;
}
.rbc-selected-cell {
background-color: rgba(33, 150, 243, 0.1);
}
// Override toolbar
.rbc-toolbar {
margin-bottom: 16px;
padding: 8px;
}
.rbc-toolbar button {
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.rbc-toolbar button:hover {
background: #f5f5f5;
border-color: #999;
}
.rbc-toolbar button.rbc-active {
background: #2196f3;
color: white;
border-color: #2196f3;
}
🔨 Step 5: Adjust Import Paths
Component menggunakan alias import. Update sesuai dengan project Anda:
Update di GenericCalendar.tsx
// ❌ Before
import dayjs from '@/utils/Helpers/dayjsSetup';
// ✅ After - adjust to your project structure
import dayjs from 'dayjs';
// atau
import dayjs from '@/lib/dayjs';
Update di CustomEvent.tsx
// ❌ Before
import dayjs from '@/utils/Helpers/dayjsSetup';
import { Tooltip } from '@mantine/core';
// ✅ After
import dayjs from 'dayjs';
import { Tooltip } from '@mantine/core';
// atau gunakan tooltip library lain
Update di CustomToolbar.tsx
// ❌ Before
import dayjs from '@/utils/Helpers/dayjsSetup';
import { Button, Flex, Group, Stack, Title } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { IconArrowLeft, IconArrowRight, IconCalendar } from '@tabler/icons-react';
// ✅ After - adjust imports
import dayjs from 'dayjs';
import { Button } from '@/components/ui/button'; // shadcn/ui
import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react'; // icons
🌐 Step 6: Setup Day.js Plugins
Calendar component menggunakan Day.js dengan plugins:
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import isBetween from 'dayjs/plugin/isBetween';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import weekday from 'dayjs/plugin/weekday';
import localeData from 'dayjs/plugin/localeData';
import 'dayjs/locale/id';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isBetween);
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
dayjs.extend(weekday);
dayjs.extend(localeData);
dayjs.locale('id'); // Set default locale ke Indonesia
export default dayjs;
Lalu import dayjs dari file ini di semua component Calendar.
🎯 Step 7: Basic Implementation
Create API Service
import axios from 'axios';
export interface EventData {
id: string;
title: string;
start: string;
end: string;
color?: string;
description?: string;
}
export const eventService = {
// Fetch events dengan date range
getEvents: async (start: Date, end: Date): Promise<EventData[]> => {
const response = await axios.get('/api/events', {
params: {
start: start.toISOString(),
end: end.toISOString(),
},
});
return response.data;
},
// Create new event
createEvent: async (event: Omit<EventData, 'id'>): Promise<EventData> => {
const response = await axios.post('/api/events', event);
return response.data;
},
// Update event
updateEvent: async (id: string, event: Partial<EventData>): Promise<EventData> => {
const response = await axios.put(`/api/events/${id}`, event);
return response.data;
},
// Delete event
deleteEvent: async (id: string): Promise<void> => {
await axios.delete(`/api/events/${id}`);
},
};
Create Calendar Page
import { GenericOptimizedCalendar } from '@/components/Calendar/GenericCalendar';
import { eventService, EventData } from '@/services/eventService';
import { useState } from 'react';
import { SlotInfo } from 'react-big-calendar';
export default function CalendarPage() {
const [selectedSlot, setSelectedSlot] = useState<SlotInfo | null>(null);
const fetchEvents = async (start: Date, end: Date) => {
return eventService.getEvents(start, end);
};
const handleSelectSlot = (slotInfo: SlotInfo) => {
setSelectedSlot(slotInfo);
// Open modal atau form untuk create event
console.log('Selected slot:', slotInfo);
};
const handleSelectEvent = (event: EventData) => {
console.log('Selected event:', event);
// Open modal untuk edit/delete event
};
return (
<div className="calendar-container" style={{ height: '100vh', padding: '20px' }}>
<h1>My Calendar</h1>
<div style={{ height: 'calc(100% - 60px)' }}>
<GenericOptimizedCalendar<EventData>
onFetchEvents={fetchEvents}
onSelectSlot={handleSelectSlot}
onSelectEventSimple={handleSelectEvent}
defaultView="month"
getEventColor={(event) => event.color || '#3174ad'}
/>
</div>
</div>
);
}
🔄 Step 8: Implement CRUD Operations
With Modal/Dialog
import { useState, useRef } from 'react';
import { GenericOptimizedCalendar, GenericCalendarRef } from '@/components/Calendar/GenericCalendar';
import { eventService, EventData } from '@/services/eventService';
import { SlotInfo } from 'react-big-calendar';
export default function CalendarWithCRUD() {
const calendarRef = useRef<GenericCalendarRef>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedSlot, setSelectedSlot] = useState<SlotInfo | null>(null);
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null);
const fetchEvents = async (start: Date, end: Date) => {
return eventService.getEvents(start, end);
};
const handleSelectSlot = (slotInfo: SlotInfo) => {
setSelectedSlot(slotInfo);
setSelectedEvent(null);
setIsModalOpen(true);
};
const handleSelectEvent = (event: EventData) => {
setSelectedEvent(event);
setSelectedSlot(null);
setIsModalOpen(true);
};
const handleCreateEvent = async (eventData: Omit<EventData, 'id'>) => {
await eventService.createEvent(eventData);
setIsModalOpen(false);
calendarRef.current?.refresh(); // Refresh calendar
};
const handleUpdateEvent = async (id: string, eventData: Partial<EventData>) => {
await eventService.updateEvent(id, eventData);
setIsModalOpen(false);
calendarRef.current?.refresh();
};
const handleDeleteEvent = async (id: string) => {
await eventService.deleteEvent(id);
setIsModalOpen(false);
calendarRef.current?.refresh();
};
return (
<div style={{ height: '100vh', padding: '20px' }}>
<GenericOptimizedCalendar<EventData>
ref={calendarRef}
onFetchEvents={fetchEvents}
onSelectSlot={handleSelectSlot}
onSelectEventSimple={handleSelectEvent}
getEventColor={(event) => event.color}
/>
{/* Your Modal Component */}
{isModalOpen && (
<EventModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
selectedSlot={selectedSlot}
selectedEvent={selectedEvent}
onCreate={handleCreateEvent}
onUpdate={handleUpdateEvent}
onDelete={handleDeleteEvent}
/>
)}
</div>
);
}
⚙️ Step 9: Configuration Options
Production Setup
export const calendarConfig = {
// Default view
defaultView: 'month' as const,
// Working hours
timeWindow: {
start: '08:00',
end: '17:00',
},
// Working days (0 = Sunday, 1 = Monday, ...)
workingDays: [1, 2, 3, 4, 5], // Senin - Jumat
// React Query config
staleTimeMs: 60000, // 1 minute
// Feature flags
features: {
preventOverlap: true,
allowBackDate: false,
allowForwardDate: true,
restrictVisibleHours: true,
},
// Colors
colors: {
default: '#3174ad',
pending: '#ff9800',
approved: '#4caf50',
rejected: '#f44336',
},
};
Use Config
import { calendarConfig } from '@/config/calendar.config';
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
defaultView={calendarConfig.defaultView}
timeWindow={calendarConfig.timeWindow}
workingDays={calendarConfig.workingDays}
staleTimeMs={calendarConfig.staleTimeMs}
preventOverlap={calendarConfig.features.preventOverlap}
allowBackDate={calendarConfig.features.allowBackDate}
allowForwardDate={calendarConfig.features.allowForwardDate}
restrictVisibleHours={calendarConfig.features.restrictVisibleHours}
/>
🎨 Step 10: Customize UI
Without Mantine (Use Your Own UI Library)
Replace CustomEvent.tsx
import React, { FC, memo } from 'react';
import { EventProps } from 'react-big-calendar';
import { CalendarEvent } from './types';
const CustomEventPlain: FC<EventProps<CalendarEvent>> = ({ event }) => {
return (
<div
className="custom-event"
title={event.title}
style={{
padding: '4px 8px',
fontSize: '13px',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
<strong>{event.title}</strong>
{event.desc && (
<div style={{ fontSize: '11px', opacity: 0.9 }}>
{event.desc}
</div>
)}
</div>
);
};
export default memo(CustomEventPlain);
Replace CustomToolbar.tsx (Simplified)
import React, { FC } from 'react';
import { Navigate, ToolbarProps, View } from 'react-big-calendar';
import { CalendarEvent } from './types';
const CustomToolbarPlain: FC<ToolbarProps<CalendarEvent>> = ({
date,
view,
onNavigate,
onView,
}) => {
const navigate = (action: 'TODAY' | 'PREV' | 'NEXT') => {
if (action === 'TODAY') onNavigate(Navigate.TODAY);
else if (action === 'NEXT') onNavigate(Navigate.NEXT);
else onNavigate(Navigate.PREVIOUS);
};
return (
<div className="calendar-toolbar">
<div className="nav-buttons">
<button onClick={() => navigate('TODAY')}>Today</button>
<button onClick={() => navigate('PREV')}>Previous</button>
<button onClick={() => navigate('NEXT')}>Next</button>
</div>
<div className="title">
{date.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' })}
</div>
<div className="view-buttons">
<button onClick={() => onView('month')}>Month</button>
<button onClick={() => onView('week')}>Week</button>
<button onClick={() => onView('day')}>Day</button>
</div>
</div>
);
};
export default CustomToolbarPlain;
✅ Step 11: Testing
Test Component
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GenericOptimizedCalendar } from '@/components/Calendar/GenericCalendar';
describe('Calendar Component', () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
const mockFetchEvents = jest.fn().mockResolvedValue([
{
id: '1',
title: 'Test Event',
start: new Date('2025-12-05T09:00:00'),
end: new Date('2025-12-05T10:00:00'),
},
]);
it('renders calendar with events', async () => {
render(
<QueryClientProvider client={queryClient}>
<div style={{ height: '600px' }}>
<GenericOptimizedCalendar onFetchEvents={mockFetchEvents} />
</div>
</QueryClientProvider>
);
await waitFor(() => {
expect(mockFetchEvents).toHaveBeenCalled();
});
expect(screen.getByText('Test Event')).toBeInTheDocument();
});
});
🐛 Troubleshooting
Common Issues
1. Calendar tidak tampil / height 0px
Problem: Container tidak memiliki explicit height.
Solution:
// ❌ Wrong
<GenericOptimizedCalendar onFetchEvents={fetchEvents} />
// ✅ Correct
<div style={{ height: '600px' }}>
<GenericOptimizedCalendar onFetchEvents={fetchEvents} />
</div>
2. Events tidak muncul
Problem: Format data tidak sesuai.
Solution:
// Pastikan API return format yang correct
{
id: string,
title: string,
start: Date | string, // ISO string atau Date object
end: Date | string,
}
3. Import error @/utils/...
Problem: Path alias tidak tersedia.
Solution:
Update tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Atau ganti dengan relative import:
import dayjs from '../../../lib/dayjs';
4. Module not found: react-big-calendar/lib/css
Problem: CSS tidak di-import.
Solution:
// Di App.tsx atau main.tsx
import 'react-big-calendar/lib/css/react-big-calendar.css';
5. Type error di Calendar component
Problem: React Big Calendar type issue.
Solution:
npm install --save-dev @types/react-big-calendar
📚 Next Steps
Setelah instalasi berhasil:
- ✅ Baca Component API Reference
- ✅ Lihat Usage Examples
- ✅ Customize UI sesuai design system Anda
- ✅ Implement CRUD operations
- ✅ Add tests
- ✅ Deploy to production
🔗 Resources
Selamat! Calendar component sudah siap digunakan di project Anda 🎉
Jika ada pertanyaan atau issue saat instalasi, silakan review dokumentasi atau check source code component.