Skip to main content

๐Ÿ“… 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โ€‹
PropTypeDefaultDescription
onFetchEvents(start: Date, end: Date, params?) => Promise<TEvent[]>RequiredFunction untuk fetch data event
queryParamsRecord<string, unknown>undefinedParameter tambahan untuk API call
defaultView'month' | 'week' | 'day' | 'agenda''month'View awal calendar
localizerDateLocalizeridLocalizerLocalizer untuk format tanggal
Time & Date Restrictionsโ€‹
PropTypeDefaultDescription
timeWindowTimeWindowundefinedBatasi jam kerja (contoh: 08:00 - 17:00)
enforceTimeWindowbooleantrueEnforce time window saat select slot
restrictVisibleHoursbooleantrueSembunyikan jam di luar time window
workingDaysnumber[]undefinedArray hari kerja (0=Minggu, 1=Senin, dst)
allowBackDatebooleantrueIzinkan select tanggal lampau
allowForwardDatebooleantrueIzinkan select tanggal depan
preventOverlapbooleanfalseCegah event overlap
Styling & Customizationโ€‹
PropTypeDefaultDescription
getEventColor(event: TEvent) => string | undefinedundefinedFunction untuk set warna event
isEventDisabled(event: TEvent) => boolean | undefinedundefinedFunction untuk disable event
disabledSlotClassNamestringundefinedClass untuk disabled slot
disabledSlotStyleCSSPropertiesundefinedStyle untuk disabled slot
renderLoadingReactNodeundefinedCustom loading indicator
React Query Optionsโ€‹
PropTypeDefaultDescription
staleTimeMsnumber30000Stale time untuk cache (ms)
queryOptionsUseQueryOptionsundefinedAdditional React Query options
Event Handlersโ€‹
PropTypeDescription
onSelectEvent(event: TEvent, e: Event) => voidHandler saat event diklik
onSelectEventSimple(event: TEvent) => voidHandler sederhana saat event diklik
onSelectSlot(slotInfo: SlotInfo) => voidHandler saat slot diklik
onSelectSlotDenied(reason: string) => voidHandler saat slot denied
Component Overridesโ€‹
PropTypeDescription
componentsPartial<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


๐Ÿค Contributingโ€‹

Jika ingin menambahkan fitur atau fix bug:

  1. Update component di GenericCalendar.tsx
  2. Update types di types.ts
  3. Update dokumentasi ini
  4. Tambahkan test case
  5. Update changelog

๐Ÿ“„ Licenseโ€‹

Component ini adalah bagian dari project internal. Silakan disesuaikan dengan kebutuhan project Anda.


๐Ÿ’ฌ Supportโ€‹

Jika ada pertanyaan atau issue:

  1. Check dokumentasi ini terlebih dahulu
  2. Review source code di components/Calendar/
  3. Lihat contoh usage di production code
  4. Contact maintainer jika masih ada masalah

Last Updated: December 5, 2025
Version: 2.0.0
Maintainer: Sisappra Development Team