๐ Calendar Component
Component Calendar yang powerful dan reusable untuk menampilkan event/jadwal dengan fitur-fitur lengkap seperti time window restriction, working days, overlap prevention, dan integrasi dengan React Query.
๐ฏ Overviewโ
Calendar component ini adalah wrapper dari React Big Calendar yang telah di-enhance dengan:
- โ Integrasi React Query - Auto-fetching & caching data
- โ Type-safe - Full TypeScript support
- โ Time Window Restriction - Batasi jam kerja
- โ Working Days - Filter hari kerja
- โ Overlap Prevention - Cegah event bentrok
- โ Custom Styling - Event colors & disabled states
- โ Localization - Bahasa Indonesia built-in
- โ Responsive - Mobile-friendly toolbar
๐ฆ Dependenciesโ
npm install react-big-calendar date-fns dayjs @tanstack/react-query
# atau
yarn add react-big-calendar date-fns dayjs @tanstack/react-query
Peer Dependencies:
- React 18+
- TypeScript 4.5+
- Mantine UI (optional, untuk Tooltip dan Button)
๐ File Structureโ
components/Calendar/
โโโ index.tsx # Main wrapper (legacy)
โโโ GenericCalendar.tsx # Generic component (recommended)
โโโ CustomEvent.tsx # Event renderer
โโโ CustomToolbar.tsx # Toolbar dengan navigasi
โโโ Calendar.module.css # Styling
โโโ types.ts # Type definitions
โโโ utils/
โโโ helper.ts # Helper functions
๐ Quick Startโ
Basic Usageโ
import { GenericOptimizedCalendar } from '@/components/Calendar/GenericCalendar';
import type { CalendarEvent } from '@/components/Calendar/types';
function MyCalendar() {
const fetchEvents = async (start: Date, end: Date) => {
const response = await fetch(
`/api/events?start=${start.toISOString()}&end=${end.toISOString()}`
);
return response.json();
};
return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
defaultView="month"
/>
);
}
๐ API Referenceโ
GenericOptimizedCalendar<TEvent>โ
Generic calendar component dengan full type safety.
Propsโ
Core Propsโ
| Prop | Type | Default | Description |
|---|---|---|---|
onFetchEvents | (start: Date, end: Date, params?) => Promise<TEvent[]> | Required | Function untuk fetch data event |
queryParams | Record<string, unknown> | undefined | Parameter tambahan untuk API call |
defaultView | 'month' | 'week' | 'day' | 'agenda' | 'month' | View awal calendar |
localizer | DateLocalizer | idLocalizer | Localizer untuk format tanggal |
Time & Date Restrictionsโ
| Prop | Type | Default | Description |
|---|---|---|---|
timeWindow | TimeWindow | undefined | Batasi jam kerja (contoh: 08:00 - 17:00) |
enforceTimeWindow | boolean | true | Enforce time window saat select slot |
restrictVisibleHours | boolean | true | Sembunyikan jam di luar time window |
workingDays | number[] | undefined | Array hari kerja (0=Minggu, 1=Senin, dst) |
allowBackDate | boolean | true | Izinkan select tanggal lampau |
allowForwardDate | boolean | true | Izinkan select tanggal depan |
preventOverlap | boolean | false | Cegah event overlap |
Styling & Customizationโ
| Prop | Type | Default | Description |
|---|---|---|---|
getEventColor | (event: TEvent) => string | undefined | undefined | Function untuk set warna event |
isEventDisabled | (event: TEvent) => boolean | undefined | undefined | Function untuk disable event |
disabledSlotClassName | string | undefined | Class untuk disabled slot |
disabledSlotStyle | CSSProperties | undefined | Style untuk disabled slot |
renderLoading | ReactNode | undefined | Custom loading indicator |
React Query Optionsโ
| Prop | Type | Default | Description |
|---|---|---|---|
staleTimeMs | number | 30000 | Stale time untuk cache (ms) |
queryOptions | UseQueryOptions | undefined | Additional React Query options |
Event Handlersโ
| Prop | Type | Description |
|---|---|---|
onSelectEvent | (event: TEvent, e: Event) => void | Handler saat event diklik |
onSelectEventSimple | (event: TEvent) => void | Handler sederhana saat event diklik |
onSelectSlot | (slotInfo: SlotInfo) => void | Handler saat slot diklik |
onSelectSlotDenied | (reason: string) => void | Handler saat slot denied |
Component Overridesโ
| Prop | Type | Description |
|---|---|---|
components | Partial<Components<TEvent>> | Override RBC components |
Typesโ
CalendarEventโ
Interface standar untuk event:
interface CalendarEvent {
id: string;
title: string;
start: Date | string;
end: Date | string;
color?: string;
desc?: string;
disabled?: boolean;
allDay?: boolean;
resourceId?: string | number;
}
TimeWindowโ
Type untuk time restriction:
type TimeWindow =
| { start: string | Date; end: string | Date }
| ((date: Date) => { start: string | Date; end: string | Date });
Example:
// Static time window
const timeWindow = { start: '08:00', end: '17:00' };
// Dynamic time window
const timeWindow = (date: Date) => {
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
return isWeekend
? { start: '09:00', end: '15:00' }
: { start: '08:00', end: '17:00' };
};
GenericCalendarRefโ
Ref API untuk control calendar:
interface GenericCalendarRef {
refresh: () => void;
}
๐ก Usage Examplesโ
Example 1: Basic Calendar dengan APIโ
import { GenericOptimizedCalendar } from '@/components/Calendar/GenericCalendar';
interface MyEvent {
id: string;
title: string;
start: string;
end: string;
color?: string;
}
function BasicCalendar() {
const fetchEvents = async (start: Date, end: Date) => {
const res = await fetch(`/api/events?start=${start}&end=${end}`);
return res.json() as Promise<MyEvent[]>;
};
return (
<div style={{ height: '600px' }}>
<GenericOptimizedCalendar<MyEvent>
onFetchEvents={fetchEvents}
defaultView="month"
getEventColor={(event) => event.color || '#3174ad'}
/>
</div>
);
}
Example 2: Working Hours & Working Daysโ
function WorkingHoursCalendar() {
const fetchEvents = async (start: Date, end: Date) => {
// Your API call
return [];
};
return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
timeWindow={{ start: '08:00', end: '17:00' }}
workingDays={[1, 2, 3, 4, 5]} // Senin - Jumat
enforceTimeWindow={true}
restrictVisibleHours={true}
disabledSlotStyle={{
backgroundColor: '#f5f5f5',
cursor: 'not-allowed',
}}
onSelectSlotDenied={(reason) => {
console.log('Slot denied:', reason);
}}
/>
);
}
Example 3: Prevent Overlapโ
function NoOverlapCalendar() {
const fetchEvents = async (start: Date, end: Date) => {
// Your API call
return [];
};
return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
preventOverlap={true}
onSelectSlotDenied={(reason) => {
if (reason === 'overlap') {
alert('Slot ini sudah terisi!');
}
}}
/>
);
}
Example 4: Custom Event Colors & Disabled Stateโ
interface MyEvent {
id: string;
title: string;
start: string;
end: string;
status: 'pending' | 'approved' | 'rejected';
isPast: boolean;
}
function ColoredCalendar() {
const getEventColor = (event: MyEvent) => {
if (event.status === 'approved') return '#4caf50';
if (event.status === 'rejected') return '#f44336';
return '#ff9800'; // pending
};
const isEventDisabled = (event: MyEvent) => {
return event.isPast || event.status === 'rejected';
};
return (
<GenericOptimizedCalendar<MyEvent>
onFetchEvents={fetchMyEvents}
getEventColor={getEventColor}
isEventDisabled={isEventDisabled}
/>
);
}
Example 5: With Ref & Manual Refreshโ
import { useRef } from 'react';
import { GenericCalendarRef } from '@/components/Calendar/GenericCalendar';
function RefreshableCalendar() {
const calendarRef = useRef<GenericCalendarRef>(null);
const handleRefresh = () => {
calendarRef.current?.refresh();
};
return (
<div>
<button onClick={handleRefresh}>Refresh Calendar</button>
<GenericOptimizedCalendar
ref={calendarRef}
onFetchEvents={fetchEvents}
/>
</div>
);
}
Example 6: Custom Toolbarโ
import CustomToolbar from '@/components/Calendar/CustomToolbar';
function CalendarWithCustomToolbar() {
return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
components={{
toolbar: (props) => <CustomToolbar {...props} loading={false} />,
}}
/>
);
}
Example 7: Axios dengan Query Paramsโ
import axios from 'axios';
function AxiosCalendar() {
const fetchEvents = async (
start: Date,
end: Date,
params?: Record<string, unknown>
) => {
const response = await axios.get('/api/events', {
params: {
start: start.toISOString(),
end: end.toISOString(),
...params,
},
});
return response.data;
};
return (
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
queryParams={{
userId: '123',
status: 'active'
}}
staleTimeMs={60000} // Cache 1 menit
/>
);
}
๐จ Stylingโ
CSS Modulesโ
Component menggunakan CSS modules untuk styling. File: Calendar.module.css
/* Custom event styling */
.customEvent {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
border-radius: 6px;
transition: all 0.25s cubic-bezier(0.08, 0.82, 0.17, 1);
}
.customEvent:hover {
transform: translateY(-2px);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.2);
}
/* Disabled event */
.customEventDisabled {
background-color: #b0bec5 !important;
cursor: not-allowed;
opacity: 0.7;
}
/* Disabled slot */
.disabledSlot {
background-color: #f1f1f1;
cursor: not-allowed;
background-image: repeating-linear-gradient(
45deg,
#e9e9e9,
#e9e9e9 5px,
#f1f1f1 5px,
#f1f1f1 10px
);
}
Override Stylesโ
Anda bisa override style dengan prop:
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
disabledSlotStyle={{
backgroundColor: '#ffe6e6',
border: '1px dashed #ff0000',
}}
/>
๐ง Advanced Configurationโ
Dynamic Time Windowโ
const getDynamicTimeWindow = (date: Date) => {
const day = date.getDay();
const isWeekend = day === 0 || day === 6;
if (isWeekend) {
return { start: '10:00', end: '14:00' };
}
// Jumat: tutup lebih cepat
if (day === 5) {
return { start: '08:00', end: '16:00' };
}
// Hari kerja normal
return { start: '08:00', end: '17:00' };
};
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
timeWindow={getDynamicTimeWindow}
/>
Custom Event Componentโ
import { EventProps } from 'react-big-calendar';
const MyCustomEvent = ({ event }: EventProps<MyEvent>) => {
return (
<div style={{ padding: '4px' }}>
<strong>{event.title}</strong>
{event.description && <div>{event.description}</div>}
</div>
);
};
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
components={{
event: MyCustomEvent,
}}
/>
Integration dengan Formโ
import { useState } from 'react';
import { SlotInfo } from 'react-big-calendar';
function CalendarWithForm() {
const [selectedSlot, setSelectedSlot] = useState<SlotInfo | null>(null);
const handleSelectSlot = (slotInfo: SlotInfo) => {
setSelectedSlot(slotInfo);
// Open form modal
};
return (
<>
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
onSelectSlot={handleSelectSlot}
/>
{selectedSlot && (
<FormModal
start={selectedSlot.start}
end={selectedSlot.end}
onClose={() => setSelectedSlot(null)}
/>
)}
</>
);
}
๐ Data Normalizationโ
Component secara otomatis normalize data dari API menggunakan normalizeEvents:
// Input dari API (flexible format)
const apiResponse = [
{
id: '1',
title: 'Meeting',
start: '2025-12-05T09:00:00Z',
end: '2025-12-05T10:00:00Z',
customField: 'value',
}
];
// Output (normalized)
const normalized = [
{
id: '1',
title: 'Meeting',
start: new Date('2025-12-05T09:00:00'),
end: new Date('2025-12-05T10:00:00'),
color: '',
disabled: false,
desc: '',
}
];
Features:
- Auto-generate ID jika tidak ada
- Convert string date ke Date object
- Remove timezone (Z) untuk local time
- Deduplication berdasarkan id + start + end
- Default values untuk optional fields
๐ React Query Integrationโ
Component menggunakan React Query untuk:
Automatic Cachingโ
- Cache duration:
staleTimeMs(default 30 detik) - Auto refetch saat navigate ke view baru
- Background refetch support
Query Key Strategyโ
// Query key berdasarkan:
// 1. startDate
// 2. endDate
// 3. queryParams
const queryKey = [
'calendar-events',
startDate.toISOString(),
endDate.toISOString(),
JSON.stringify(queryParams)
];
Manual Refetchโ
const calendarRef = useRef<GenericCalendarRef>(null);
// Trigger refetch
calendarRef.current?.refresh();
๐ Localizationโ
Default locale: Bahasa Indonesia
Menggunakan date-fns dengan locale id:
import { id as idLocale } from 'date-fns/locale';
export const idLocalizer: DateLocalizer = dateFnsLocalizer({
format: (date: Date, formatStr: string) =>
dfFormat(date, formatStr, { locale: idLocale }),
parse: (value: string, formatStr: string, referenceDate: Date) =>
dfParse(value, formatStr, referenceDate, { locale: idLocale }),
startOfWeek: (date: Date) =>
dfStartOfWeek(date, { weekStartsOn: 1, locale: idLocale }),
getDay: (date: Date) => dfGetDay(date),
locales: { 'id-ID': idLocale },
});
Output:
- Hari: Senin, Selasa, Rabu, dst
- Bulan: Januari, Februari, Maret, dst
- Format tanggal: DD/MM/YYYY
Custom Localeโ
import { enUS } from 'date-fns/locale';
const enLocalizer = dateFnsLocalizer({
format: (date, formatStr) =>
dfFormat(date, formatStr, { locale: enUS }),
// ... other config
});
<GenericOptimizedCalendar
localizer={enLocalizer}
culture="en-US"
onFetchEvents={fetchEvents}
/>
โ ๏ธ Common Pitfallsโ
1. Missing Heightโ
Calendar requires explicit height:
// โ Wrong
<GenericOptimizedCalendar onFetchEvents={fetchEvents} />
// โ
Correct
<div style={{ height: '600px' }}>
<GenericOptimizedCalendar onFetchEvents={fetchEvents} />
</div>
2. Timezone Issuesโ
API response dengan timezone (Z) akan di-convert ke local time:
// Input: '2025-12-05T09:00:00Z'
// Output: Date object di local timezone
// Jika ingin keep UTC, disable normalization:
const events = await onFetchEvents(start, end);
// Process manually without normalizeEvents
3. Event Not Showingโ
Pastikan event memiliki start dan end:
// โ Wrong
{ id: '1', title: 'Event' }
// โ
Correct
{
id: '1',
title: 'Event',
start: new Date(),
end: new Date()
}
4. Overlap Prevention Not Workingโ
preventOverlap hanya mencegah select slot, bukan duplicate event dari API:
// Validasi overlap di API level
const handleCreateEvent = async (newEvent) => {
// Check overlap
const hasOverlap = await checkOverlap(newEvent);
if (hasOverlap) {
alert('Event overlap!');
return;
}
await createEvent(newEvent);
};
๐งช Testingโ
Unit Test Exampleโ
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GenericOptimizedCalendar } from './GenericCalendar';
describe('GenericOptimizedCalendar', () => {
const queryClient = new QueryClient();
const mockFetchEvents = jest.fn().mockResolvedValue([
{
id: '1',
title: 'Test Event',
start: new Date(),
end: new Date(),
}
]);
it('renders calendar and fetches 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();
});
});
๐ Migration Guideโ
From Legacy Calendar (index.tsx) to GenericCalendarโ
Before:
import OptimizedCalendar from '@/components/Calendar';
<OptimizedCalendar
queryKey="my-events"
queryFn={fetchEvents}
// ...props
/>
After:
import { GenericOptimizedCalendar } from '@/components/Calendar/GenericCalendar';
<GenericOptimizedCalendar
onFetchEvents={fetchEvents}
// ...props
/>
Key Changes:
queryFnโonFetchEvents- No more
queryKey(auto-generated) - Better TypeScript support
- More flexible time window API
๐ Related Resourcesโ
๐ค Contributingโ
Jika ingin menambahkan fitur atau fix bug:
- Update component di
GenericCalendar.tsx - Update types di
types.ts - Update dokumentasi ini
- Tambahkan test case
- Update changelog
๐ Licenseโ
Component ini adalah bagian dari project internal. Silakan disesuaikan dengan kebutuhan project Anda.
๐ฌ Supportโ
Jika ada pertanyaan atau issue:
- Check dokumentasi ini terlebih dahulu
- Review source code di
components/Calendar/ - Lihat contoh usage di production code
- Contact maintainer jika masih ada masalah
Last Updated: December 5, 2025
Version: 2.0.0
Maintainer: Sisappra Development Team