guava/calendar
Guava Calendar is a Laravel package for building calendars and managing events with recurring schedules, customizable views, and Blade-friendly integration. Add event scheduling and calendar UIs to your app quickly and cleanly.
Install and wire it up in three moves.
composer require guava/calendar
Then register the plugin in your panel provider (app/Providers/Filament/AdminPanelProvider.php):
->plugin(GuavaCalendarPlugin::make())
Then rebuild assets. As of 3.2.2 the calendar JS (@event-calendar/core) ships bundled with the package from npm rather than being pulled from a CDN — so if your build pipeline doesn't pick up the package's assets you get an empty container, not an error. Verify the class name against src/ before you copy-paste from a blog post; the public surface is small enough to read in a few minutes.
Where to look first — in this order:
workbench/ in the package repo. 3.2.2 added a real Filament panel there for the maintainer's own manual testing. It reflects currently-intended wiring, which is worth more than the README.Month, Week, Day, List). It was completed in 3.2.2 — if you're on an earlier 3.2.x you may find entries missing and wonder why list won't register.Your first use case: a read-only month view over one model. Pick something with a start and an end — Appointment, Shift, Booking. Map records to events:
protected function getEvents(): array
{
return Appointment::query()
->whereBetween('starts_at', [$this->rangeStart, $this->rangeEnd])
->get()
->map(fn ($a) => [
'id' => $a->id,
'title' => $a->customer_name,
'start' => $a->starts_at->toIso8601String(),
'end' => $a->ends_at->toIso8601String(),
'color' => $a->status_color,
])
->all();
}
Render it, confirm events appear, and only then add drag/drop and context menus. A broken event source and a broken context menu both present as "calendar is blank or won't respond" — you want to be debugging one layer at a time.
One widget class per calendar surface. Don't build a configurable mega-widget with ->calendar('mine') switches. Make MyCalendarWidget, TeamCalendarWidget, AppointmentCalendarWidget. You get per-widget canView() authorization for free, and 3.2.2's scoping fix (context-menu lookups bound to the owning widget) was written specifically to make this shape safe. Compose widgets onto dashboards or resource pages as ordinary Filament widgets.
Context menus as Filament Actions — the payoff pattern. Right-click → Reschedule / Reassign / Cancel is now first-class: mounted actions receive the context data they were invoked against, and the menu only opens once its actions resolve. Structure every context action the same way:
Action::make('cancel')
->requiresConfirmation()
->action(function (array $context) {
$appointment = Appointment::findOrFail($context['id']); // re-query
Gate::authorize('cancel', $appointment); // re-authorize
$appointment->cancel(); // act
})
Never trust the context payload as authoritative. It crosses a Livewire round trip; treat it exactly like a request parameter. Re-query, re-authorize, then act. This also gives you an auditable trail without a detail-page round trip.
Keep action lists short — three to five items. Anything needing a real form (a date picker for rescheduling, a select for reassignment) goes in a modal action, not a page redirect.
Separate the event source from the widget. The calendar always asks for a date range. Put range logic in one place — a query scope, an invokable action, or a small builder class taking (Carbon $start, Carbon $end) and returning an array of events. Inline the query in the widget and you'll re-derive range-bounded filtering every time you add a view type or a second source. Use Carbon for start/end and let the package handle formatting.
Multiple calendars on one screen. "My calendar + team calendar" works, but only because each is its own widget with its own event source and its own action list. Upgrading from ≤3.2.1 with multiple calendars on a page: budget a regression pass. Old code that relied on the previous global lookup may now bind actions correctly instead of working by accident — which reads as a behavior change in QA.
Lean into SPA navigation. 3.2.2 destroys the calendar on unmount, so wire:navigate between calendar pages no longer duplicates the widget. This means calendars can live on ordinary Filament resource pages and users can navigate freely. Do not add wire:ignore workarounds for duplication — they mask the symptom and reintroduce the leak the fix just closed.
Prototype in the workbench, verify in the host app. Two separate failure domains: PHP-side configuration (is my event array correct?) and asset-side delivery (did the JS load?). The workbench panel isolates the first; your app's build pipeline tests the second. Debugging both simultaneously is how an afternoon disappears.
Upgrade path is low-risk. The 3.2.x line is additive and stabilizing — 3.2.2 is five targeted fixes plus two chores, no breaking changes. Roadmap estimates for scheduling/shift-planning/booking views don't need a rewrite-risk buffer.
The 3.2.2 asset change is the one thing semver lies about. Bundling @event-calendar/core from npm instead of loading it from a CDN is the right call — but it changes asset provenance inside a patch release. If you previously pinned a CDN version, customized asset loading, or added a CSP allowlist entry for the CDN, re-verify the entire pipeline. Failure mode: a silently blank calendar container with no obvious console error. Clear compiled views, config cache, and any asset cache, then rebuild.
Pin the Filament constraint — don't let ^ resolve. The package tracks Filament versions actively (the 3.2.2 workbench chore implies it), but the supported range isn't loudly documented. Pin explicitly in composer.json and re-check on every Filament major upgrade. A composer update that silently crosses a Filament major is a bad afternoon of mysterious Livewire errors.
"Right-click does nothing" is a PHP bug, not a JS bug. Post-3.2.2 the menu only opens once its actions resolve. If your action-list builder is slow or throws, the menu simply never appears. Check storage/logs/laravel.log before you open devtools.
Recurrence, conflicts, and timezones are yours to build. Recurrence expansion with exceptions, availability rules, conflict detection, and timezone-correct booking are explicitly out of scope. This is a view layer. If a stakeholder asks for "a booking system," scope the engine before you agree — the package covers the pixels, not the logic.
Give duplicate instances distinct identity. If you render the same widget class twice on a page (e.g. two date ranges, or two team calendars), give each a distinguishable key/parameter. Post-3.2.2 scoping makes the action resolution correct, but you still need to tell them apart when debugging.
A debugging order that actually works:
Log::info() it. If empty, your range filter or Carbon casting is wrong.Each step isolates exactly one layer.
Extension points, in order of safety: custom event arrays → custom actions → Blade view wrappers → custom view types (only if the enum already supports them) → patching the JS. Stop before the last one. If you need grid rendering customization beyond what @event-calendar/core exposes, that's a signal to reconsider the dependency — not to fork it.
Assess the maintenance posture for anything load-bearing. 315 stars, zero declared dependents, single-maintainer footprint, no commercial backing or SLA. Fine for internal admin surfaces and back-office tooling. Think hard before making it the foundation of a customer-facing revenue flow. Watch release cadence over a couple of months before committing a roadmap to it.
Where it genuinely wins: you're already on Filament, the calendar is an admin/internal surface, you need display plus lightweight interaction, and you'd rather write PHP configuration than a bespoke front-end. The npm-bundled assets also make it a real qualifier for air-gapped, strict-CSP, or data-residency-constrained deployments where a CDN-dependent alternative would be disqualified outright.
How can I help you explore Laravel packages today?