Skip to main content

🚀 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
info

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

  1. GenericCalendar.tsx - Component utama
  2. types.ts - Type definitions
  3. CustomEvent.tsx - Custom event renderer
  4. CustomToolbar.tsx - Custom toolbar
  5. Calendar.module.css - Styling
  6. utils/helper.ts - Helper functions

🔧 Step 3: Setup React Query

App-level Setup

Wrap aplikasi dengan QueryClientProvider:

App.tsx
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

App.tsx or main.tsx
// Import RBC default styles
import 'react-big-calendar/lib/css/react-big-calendar.css';

Optional: Custom SCSS/CSS

Jika ingin override RBC styles:

styles/calendar-custom.scss
// 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:

lib/dayjs.ts
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

services/eventService.ts
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

pages/CalendarPage.tsx
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

pages/CalendarWithCRUD.tsx
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

config/calendar.config.ts
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

Calendar/CustomEventPlain.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)

Calendar/CustomToolbarPlain.tsx
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

__tests__/Calendar.test.tsx
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:

  1. ✅ Baca Component API Reference
  2. ✅ Lihat Usage Examples
  3. ✅ Customize UI sesuai design system Anda
  4. ✅ Implement CRUD operations
  5. ✅ Add tests
  6. ✅ 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.