@theposeidonas/reservation-calendar
v0.2.0
Published
A framework-agnostic reservation calendar built for wedding photographers. Month / week / day views, package color indicators, fixed service grid, and payment tracking.
Maintainers
Readme
reservationCalendar
A React reservation calendar built for wedding photographers. Month / week / day views, package color indicators, a 12-service grid, and payment tracking.
Features
- Three views — month grid, week timegrid, day timegrid
- Visual package indicators — color stripe or fill tint, plus a letter badge for colorblind accessibility
- 12-service grid — fixed-position slots so users learn the layout; each slot optionally accepts a custom SVG icon
- Smart month overflow — max N reservations per cell; the rest collapse into a day-list modal
- Hairdresser time — separate timestamp shown in the reservation detail modal
- Payment tracking — paid / remaining amounts with a progress bar
- Light + dark theme out of the box, overridable via CSS custom properties
- Heroicons by default — override any service slot with a raw SVG string
- ~9 KB gzipped (no runtime dependencies beyond React ≥ 17)
Installation
npm install @theposeidonas/reservation-calendaryarn add @theposeidonas/reservation-calendarpnpm add @theposeidonas/reservation-calendarQuick start
import ReservationCalendar from '@theposeidonas/reservation-calendar';
import '@theposeidonas/reservation-calendar/dist/style.css';
const packages = {
gold: { id: 'gold', label: 'Gold', color: '#d4a557', short: 'G' },
platinum: { id: 'platinum', label: 'Platinum', color: '#3a3a44', short: 'P' },
engagement: { id: 'engagement', label: 'Söz / Nişan', color: '#c97296', short: 'N' },
};
const services = [
{ id: 'wedding', label: 'Düğün' },
{ id: 'engage', label: 'Söz / Nişan' },
{ id: 'savedate', label: 'Save the Date' },
{ id: 'after', label: 'After Wedding' },
{ id: 'outdoor', label: 'Dış Çekim' },
{ id: 'video', label: 'Video' },
{ id: 'drone', label: 'Drone' },
{ id: 'clip', label: 'Klip' },
{ id: 'album', label: 'Albüm' },
{ id: 'studio', label: 'Stüdyo' },
{ id: 'booth', label: 'Foto Kabin' },
{ id: 'express', label: 'Hızlı Teslim' },
];
const reservations = [
{
id: 'r1',
title: 'Burcu & Baran',
start: new Date('2026-05-15T14:00'),
end: new Date('2026-05-15T18:00'),
packageId: 'gold',
hairdresserAt: new Date('2026-05-15T10:00'),
venue: 'Conrad Bosphorus',
phone: '+90 555 555 55 55',
services: [true, false, true, false, true, true, false, false, true, true, false, false],
totalPrice: 20000,
paidAmount: 8000,
notes: 'Hazırlık 2 saat önce başlayacak.',
},
];
export default function App() {
return (
<ReservationCalendar
reservations={reservations}
packages={packages}
services={services}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| reservations | Reservation[] | [] | List of reservation objects |
| packages | Record<string, Package> | built-in | Package definitions keyed by id |
| services | Service[] | built-in | Ordered list of service slots (max 12) |
| variant | 'stripe' \| 'fill' | 'fill' | Chip / event card visual style |
| badge | 'letter' \| 'dot' \| 'none' | 'letter' | Package indicator shown on chips |
| maxPerCell | number | 3 | Max reservations shown per month cell before overflow |
| defaultDark | boolean | false | Start in dark mode |
| dayStart | number | 9 | First hour shown in week/day timegrid |
| dayEnd | number | 21 | Last hour shown in week/day timegrid |
Data shapes
Reservation
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | ✓ | Unique identifier |
| title | string | ✓ | Short label, e.g. "Burcu & Baran" |
| start | Date | ✓ | Start datetime |
| end | Date | ✓ | End datetime |
| packageId | string | ✓ | Key into your packages map |
| services | boolean[] | | Activation flags for each service slot (index-aligned with the services prop) |
| hairdresserAt | Date | | Hairdresser arrival time; shown as a pill in the detail modal |
| venue | string | | Venue name |
| phone | string | | Primary phone number |
| phone2 | string | | Secondary phone number |
| guests | number | | Guest count |
| totalPrice | number | | Total price in the configured currency |
| paidAmount | number | | Amount already paid |
| notes | string | | Free-text notes; displayed as a pre-formatted block |
Package
{
id: string; // must match the key used in reservations
label: string; // e.g. "Gold"
color: string; // any CSS color value
short: string; // 1-2 character badge label
}Service
{
id: string; // unique identifier
label: string; // tooltip text; leave empty for an unused placeholder slot
icon?: string; // optional raw SVG string — overrides the default Heroicon
}Service icons
By default every slot renders a fixed Heroicons outline icon. Pass an icon field on any service object to replace it with your own SVG:
const services = [
{ id: 'wedding', label: 'Düğün' }, // → default Heroicon
{ id: 'custom', label: 'Custom', icon: '<svg>...</svg>' }, // → your SVG
{ id: 'unused', label: '' }, // → placeholder, no icon rendered
];Icon requirements for best results:
- Set
fill="currentColor"orstroke="currentColor"so the icon inherits the slot's active/inactive color - Omit fixed
width/heightattributes; the component sizes the icon via CSS (60%of the slot in the grid,12 pxin the day-list row) - Pass a sanitized string — the component renders it with
dangerouslySetInnerHTMLand does not perform its own sanitization
From a PHP/Filament backend using blade-ui-kit/blade-icons:
use BladeUI\Icons\Factory as IconFactory;
$services = [
['id' => 'wedding', 'label' => 'Düğün', 'icon' => svg('heroicon-o-heart')->toHtml()],
['id' => 'video', 'label' => 'Video', 'icon' => svg('heroicon-o-video-camera')->toHtml()],
['id' => 'drone', 'label' => 'Drone', 'icon' => svg('heroicon-o-paper-airplane')->toHtml()],
// ...remaining slots
];Filament + Livewire
Feed reservations from your Eloquent model and forward clicks back to Livewire:
// app/Filament/Widgets/ReservationCalendarWidget.php
class ReservationCalendarWidget extends Widget
{
protected string $view = 'filament.widgets.reservation-calendar';
protected int|string|array $columnSpan = 'full';
public function getViewData(): array
{
return [
'reservations' => Reservation::query()
->where('tenant_id', Filament::getTenant()->id)
->with(['package'])
->get()
->map(fn ($r) => [
'id' => (string) $r->id,
'title' => $r->couple_name,
'start' => $r->start_at->toIso8601String(),
'end' => $r->end_at->toIso8601String(),
'packageId' => $r->package->slug,
'hairdresserAt' => $r->hairdresser_at?->toIso8601String(),
'venue' => $r->venue,
'phone' => $r->bride_phone,
'phone2' => $r->groom_phone,
'services' => $r->getServiceFlags(),
'totalPrice' => $r->total_price,
'paidAmount' => $r->paid_amount,
'notes' => $r->notes,
])
->all(),
];
}
}{{-- resources/views/filament/widgets/reservation-calendar.blade.php --}}
<x-filament-widgets::widget>
<div
wire:ignore
x-data
x-init="
const el = $refs.rc;
el.reservations = @js($reservations);
el.packages = @js(config('rc.packages'));
el.services = @js(config('rc.services'));
"
>
<div x-ref="rc"></div>
{{-- Mount the React component into $refs.rc via your JS bundle --}}
</div>
</x-filament-widgets::widget>Tip: Mount
ReservationCalendarinto thex-refdiv from your JS bundle withReactDOM.createRoot. Passreservations,packages, andservicesas props that Alpine sets on the element — or keep them in a Livewire-aware Alpine store and re-render on$wireevents.
Theming
Every color and dimension is exposed as a CSS custom property. Override on :root or scoped to the container:
.my-calendar-wrapper {
--rc-bg: #fdfbf6;
--rc-surface: #ffffff;
--rc-text: #1f1c18;
--rc-font: 'Manrope', system-ui, sans-serif;
--rc-radius-lg: 18px;
}Core tokens
| Token | Default (light) | Description |
|---|---|---|
| --rc-bg | #faf9f7 | Page background |
| --rc-surface | #ffffff | Card / modal surface |
| --rc-surface-2 | #f5f4f1 | Recessed surface (chips, notes) |
| --rc-border | rgba(20,18,14,.08) | Default border |
| --rc-text | #1a1815 | Primary text |
| --rc-text-2 | #5e5a52 | Secondary text |
| --rc-text-3 | #908b81 | Tertiary / muted text |
| --rc-font | 'DM Sans', system-ui | UI font stack |
| --rc-font-mono | 'JetBrains Mono', monospace | Monospace font stack |
| --rc-radius-lg | 14px | Modal / card corner radius |
| --rc-radius-sm | 6px | Chip / badge corner radius |
Dark mode is activated by setting data-rc-theme="dark" on <html> (the component handles this automatically when the user toggles the theme button).
Development
# Install dependencies
npm install
# Run the demo locally (Vite dev server)
npm run dev
# Build the library
npm run build
# Build + publish to npm
npm run releaseChangelog
0.2.0
- Custom service icons — each
Serviceobject now accepts an optionaliconfield (raw SVG string). When present it replaces the default Heroicon in both the 12-slot grid and the day-list mini icons. Empty-label placeholder slots render no icon. - Modal scroll fix — the reservation detail modal no longer clips service-slot tooltips. Scrolling is now handled by an inner
rc-modal-scrollwrapper; the outerrc-modalstaysoverflow: visiblesoposition: absolutetooltips can escape the boundary at any z-index.
0.1.1
- Initial public release
License
MIT © Baran Arda
