-
-
+
-
- @foreach($monthGrid->first() as $day)
- @include($dayOfWeekView, ['day' => $day])
- @endforeach
-
+
+
- @foreach($monthGrid as $week)
-
- @foreach($week as $day)
- @include($dayView, [
- 'componentId' => $componentId,
- 'day' => $day,
- 'dayInMonth' => $day->isSameMonth($startsAt),
- 'isToday' => $day->isToday(),
- 'events' => $getEventsForDay($day, $events),
- ])
- @endforeach
-
+
+ @foreach(['month' => 'Month', 'week' => 'Week', 'day' => 'Day'] as $mode => $label)
+
@endforeach
+ @endif
+
+
+ @includeIf($beforeCalendarView)
-
+ @if($viewMode === 'month')
+ @include('livewire-calendar::month')
+ @elseif($viewMode === 'week')
+ @include('livewire-calendar::week')
+ @else
+ @include('livewire-calendar::day-detail')
+ @endif
+
+
@includeIf($afterCalendarView)
diff --git a/resources/views/day-detail.blade.php b/resources/views/day-detail.blade.php
new file mode 100644
index 0000000..ce33a7e
--- /dev/null
+++ b/resources/views/day-detail.blade.php
@@ -0,0 +1,51 @@
+
+
+
+ {{ $selectedDate->format('l, F j') }}
+
+
+ @if($allDayEvents->isNotEmpty())
+
+
All day
+
+ @foreach($allDayEvents as $event)
+
+
{{ $event['title'] }}
+
+ @endforeach
+
+
+ @endif
+
+
+ @forelse($timedDayEvents as $event)
+
+
{{ $event['title'] }}
+
{{ $event['start_time'] ?? 'All day' }}{{ isset($event['end_time']) ? ' - '.$event['end_time'] : '' }}
+
+ @empty
+
No events for this day.
+ @endforelse
+
+
+
+
+
+
{{ $selectedDate->format('l, F j, Y') }}
+
+ @foreach($hours as $hour)
+ @php($slotEvents = $getEventsForHour($selectedDate, $hourIndexMap[$hour], $timedDayEvents))
+
+
{{ $hour }}
+
+ @foreach($slotEvents as $event)
+
+
{{ $event['title'] }}
+
{{ $event['start_time'] ?? '' }}{{ isset($event['end_time']) ? ' - '.$event['end_time'] : '' }}
+
+ @endforeach
+
+
+ @endforeach
+
+
diff --git a/resources/views/month.blade.php b/resources/views/month.blade.php
new file mode 100644
index 0000000..7cf70d0
--- /dev/null
+++ b/resources/views/month.blade.php
@@ -0,0 +1,23 @@
+
+
+
+ @foreach($monthGrid->first() as $day)
+ @include($dayOfWeekView, ['day' => $day])
+ @endforeach
+
+
+ @foreach($monthGrid as $week)
+
+ @foreach($week as $day)
+ @include($dayView, [
+ 'componentId' => $componentId,
+ 'day' => $day,
+ 'dayInMonth' => $day->isSameMonth($startsAt),
+ 'isToday' => $day->isToday(),
+ 'events' => $getEventsForDay($day, $events),
+ ])
+ @endforeach
+
+ @endforeach
+
+
diff --git a/resources/views/week.blade.php b/resources/views/week.blade.php
new file mode 100644
index 0000000..ce97b9b
--- /dev/null
+++ b/resources/views/week.blade.php
@@ -0,0 +1,66 @@
+
+
+ @foreach($weekDays as $day)
+ @php($dayEvents = $getEventsForDay($day, $events))
+
+ @endforeach
+
+
+
+
+ {{ $selectedDate->format('l, M j') }}
+
+
+ @forelse($selectedWeekEvents as $event)
+
+
{{ $event['title'] }}
+ @if(isset($event['start_time']) || isset($event['end_time']))
+
{{ $event['start_time'] ?? 'All day' }}{{ isset($event['end_time']) ? ' - '.$event['end_time'] : '' }}
+ @endif
+
+ @empty
+
No events for this day.
+ @endforelse
+
+
+
+
+
+
+
Time
+ @foreach($weekDays as $day)
+
+
{{ $day->format('D') }}
+
{{ $day->format('j') }}
+
+ @endforeach
+
+
+
+ @foreach($hours as $hour)
+
+
{{ $hour }}
+ @foreach($weekDays as $day)
+ @php($slotEvents = $getEventsForHour($day, $hourIndexMap[$hour], $events))
+
+ @foreach($slotEvents as $event)
+
{{ $event['title'] }}
+ @endforeach
+
+ @endforeach
+
+ @endforeach
+
+
diff --git a/src/LivewireCalendar.php b/src/LivewireCalendar.php
index bc42118..3726c20 100755
--- a/src/LivewireCalendar.php
+++ b/src/LivewireCalendar.php
@@ -12,24 +12,6 @@
/**
* Class LivewireCalendar
* @package Omnia\LivewireCalendar
- * @property Carbon $startsAt
- * @property Carbon $endsAt
- * @property Carbon $gridStartsAt
- * @property Carbon $gridEndsAt
- * @property int $weekStartsAt
- * @property int $weekEndsAt
- * @property string $calendarView
- * @property string $dayView
- * @property string $eventView
- * @property string $dayOfWeekView
- * @property string $beforeCalendarWeekView
- * @property string $afterCalendarWeekView
- * @property string $dragAndDropClasses
- * @property int $pollMillis
- * @property string $pollAction
- * @property boolean $dragAndDropEnabled
- * @property boolean $dayClickEnabled
- * @property boolean $eventClickEnabled
*/
class LivewireCalendar extends Component
{
@@ -59,11 +41,17 @@ class LivewireCalendar extends Component
public $dayClickEnabled;
public $eventClickEnabled;
+ public $viewMode;
+ public $selectedDate;
+ public $mobileHeaderEnabled;
+ public $swipeNavigationEnabled;
+
protected $casts = [
'startsAt' => 'date',
'endsAt' => 'date',
'gridStartsAt' => 'date',
'gridEndsAt' => 'date',
+ 'selectedDate' => 'date',
];
public function mount($initialYear = null,
@@ -81,13 +69,16 @@ public function mount($initialYear = null,
$dragAndDropEnabled = true,
$dayClickEnabled = true,
$eventClickEnabled = true,
+ $viewMode = 'month',
+ $mobileHeaderEnabled = true,
+ $swipeNavigationEnabled = true,
+ $selectedDate = null,
$extras = [])
{
$this->weekStartsAt = $weekStartsAt ?? Carbon::SUNDAY;
$this->weekEndsAt = $this->weekStartsAt == Carbon::SUNDAY
? Carbon::SATURDAY
- : collect([0,1,2,3,4,5,6])->get($this->weekStartsAt + 6 - 7)
- ;
+ : collect([0, 1, 2, 3, 4, 5, 6])->get($this->weekStartsAt + 6 - 7);
$initialYear = $initialYear ?? Carbon::today()->year;
$initialMonth = $initialMonth ?? Carbon::today()->month;
@@ -95,6 +86,11 @@ public function mount($initialYear = null,
$this->startsAt = Carbon::createFromDate($initialYear, $initialMonth, 1)->startOfDay();
$this->endsAt = $this->startsAt->clone()->endOfMonth()->startOfDay();
+ $this->selectedDate = $selectedDate ? Carbon::parse($selectedDate)->startOfDay() : Carbon::today()->startOfDay();
+ $this->viewMode = in_array($viewMode, ['month', 'week', 'day'], true) ? $viewMode : 'month';
+ $this->mobileHeaderEnabled = $mobileHeaderEnabled;
+ $this->swipeNavigationEnabled = $swipeNavigationEnabled;
+
$this->calculateGridStartsEnds();
$this->setupViews($calendarView, $dayView, $eventView, $dayOfWeekView, $beforeCalendarView, $afterCalendarView);
@@ -137,6 +133,27 @@ public function setupPoll($pollMillis, $pollAction)
$this->pollAction = $pollAction;
}
+ public function setViewMode(string $mode)
+ {
+ if (in_array($mode, ['month', 'week', 'day'], true)) {
+ $this->viewMode = $mode;
+ }
+ }
+
+ public function selectDate($year, $month, $day)
+ {
+ $this->selectedDate = Carbon::createFromDate($year, $month, $day)->startOfDay();
+ $this->startsAt = $this->selectedDate->copy()->startOfMonth();
+ $this->endsAt = $this->selectedDate->copy()->endOfMonth()->startOfDay();
+ $this->calculateGridStartsEnds();
+ }
+
+ public function goToToday()
+ {
+ $this->selectedDate = Carbon::today()->startOfDay();
+ $this->goToCurrentMonth();
+ }
+
public function goToPreviousMonth()
{
$this->startsAt->subMonthNoOverflow();
@@ -153,6 +170,38 @@ public function goToNextMonth()
$this->calculateGridStartsEnds();
}
+ public function goToPreviousWeek()
+ {
+ $this->selectedDate = $this->selectedDate->copy()->subWeek()->startOfDay();
+ $this->startsAt = $this->selectedDate->copy()->startOfMonth();
+ $this->endsAt = $this->selectedDate->copy()->endOfMonth()->startOfDay();
+ $this->calculateGridStartsEnds();
+ }
+
+ public function goToNextWeek()
+ {
+ $this->selectedDate = $this->selectedDate->copy()->addWeek()->startOfDay();
+ $this->startsAt = $this->selectedDate->copy()->startOfMonth();
+ $this->endsAt = $this->selectedDate->copy()->endOfMonth()->startOfDay();
+ $this->calculateGridStartsEnds();
+ }
+
+ public function goToPreviousDay()
+ {
+ $this->selectedDate = $this->selectedDate->copy()->subDay()->startOfDay();
+ $this->startsAt = $this->selectedDate->copy()->startOfMonth();
+ $this->endsAt = $this->selectedDate->copy()->endOfMonth()->startOfDay();
+ $this->calculateGridStartsEnds();
+ }
+
+ public function goToNextDay()
+ {
+ $this->selectedDate = $this->selectedDate->copy()->addDay()->startOfDay();
+ $this->startsAt = $this->selectedDate->copy()->startOfMonth();
+ $this->endsAt = $this->selectedDate->copy()->endOfMonth()->startOfDay();
+ $this->calculateGridStartsEnds();
+ }
+
public function goToCurrentMonth()
{
$this->startsAt = Carbon::today()->startOfMonth()->startOfDay();
@@ -163,8 +212,8 @@ public function goToCurrentMonth()
public function calculateGridStartsEnds()
{
- $this->gridStartsAt = $this->startsAt->clone()->startOfWeek($this->weekStartsAt)->shiftTimezone(config('app.timezone'));;
- $this->gridEndsAt = $this->endsAt->clone()->endOfWeek($this->weekEndsAt)->shiftTimezone(config('app.timezone'));;
+ $this->gridStartsAt = $this->startsAt->clone()->startOfWeek($this->weekStartsAt)->shiftTimezone(config('app.timezone'));
+ $this->gridEndsAt = $this->endsAt->clone()->endOfWeek($this->weekEndsAt)->shiftTimezone(config('app.timezone'));
}
/**
@@ -179,31 +228,47 @@ public function monthGrid()
$days = floor(abs($firstDayOfGrid->diffInDays($lastDayOfGrid)) + 1);
if ($days % 7 != 0) {
- throw new Exception("Livewire Calendar not correctly configured. Check initial inputs.");
+ throw new Exception('Livewire Calendar not correctly configured. Check initial inputs.');
}
$monthGrid = collect();
$currentDay = $firstDayOfGrid->clone();
- while(!$currentDay->greaterThan($lastDayOfGrid)) {
+ while (!$currentDay->greaterThan($lastDayOfGrid)) {
$monthGrid->push($currentDay->clone());
$currentDay->addDay();
}
$monthGrid = $monthGrid->chunk(7);
if ($numbersOfWeeks != $monthGrid->count()) {
- throw new Exception("Livewire Calendar calculated wrong number of weeks. Sorry :(");
+ throw new Exception('Livewire Calendar calculated wrong number of weeks. Sorry :(');
}
return $monthGrid;
}
- public function events() : Collection
+ public function weekDays(): Collection
+ {
+ $start = $this->selectedDate->copy()->startOfWeek($this->weekStartsAt);
+
+ return collect(range(0, 6))->map(function ($offset) use ($start) {
+ return $start->copy()->addDays($offset);
+ });
+ }
+
+ public function timeSlots(): Collection
+ {
+ return collect(range(0, 23))->map(function ($hour) {
+ return Carbon::createFromTime($hour)->format('g:00 A');
+ });
+ }
+
+ public function events(): Collection
{
return collect();
}
- public function getEventsForDay($day, Collection $events) : Collection
+ public function getEventsForDay($day, Collection $events): Collection
{
return $events
->filter(function ($event) use ($day) {
@@ -214,13 +279,23 @@ public function getEventsForDay($day, Collection $events) : Collection
});
}
- /**
- * Determine if an event occurs on a given day.
- * Supports legacy 'date' field and new 'start_date'/'end_date' fields.
- */
+ public function getEventsForHour(Carbon $day, int $hour, Collection $events): Collection
+ {
+ return $this->getEventsForDay($day, $events)->filter(function ($event) use ($hour) {
+ if (!isset($event['start_time'])) {
+ return false;
+ }
+
+ try {
+ return Carbon::parse($event['start_time'])->hour === $hour;
+ } catch (\Throwable $e) {
+ return false;
+ }
+ });
+ }
+
protected function eventOccursOnDay(array $event, Carbon $day): bool
{
- // New multi-day format takes precedence
if (isset($event['start_date']) && isset($event['end_date'])) {
$startDate = Carbon::parse($event['start_date'])->startOfDay();
$endDate = Carbon::parse($event['end_date'])->startOfDay();
@@ -229,7 +304,6 @@ protected function eventOccursOnDay(array $event, Carbon $day): bool
return $checkDay->between($startDate, $endDate);
}
- // Fallback to legacy 'date' field
if (isset($event['date'])) {
return Carbon::parse($event['date'])->isSameDay($day);
}
@@ -237,13 +311,8 @@ protected function eventOccursOnDay(array $event, Carbon $day): bool
return false;
}
- /**
- * Enrich event with day-specific metadata for view rendering.
- * Adds position info (is_first_day, is_last_day, is_multiday, day_position).
- */
protected function enrichEventForDay(array $event, Carbon $day): array
{
- // Legacy single-day events - add minimal metadata
if (!isset($event['start_date']) || !isset($event['end_date'])) {
$event['is_multiday'] = false;
$event['is_first_day'] = true;
@@ -272,7 +341,7 @@ protected function enrichEventForDay(array $event, Carbon $day): array
public function onDayClick($year, $month, $day)
{
- //
+ $this->selectDate($year, $month, $day);
}
public function onEventClick($eventId)
@@ -287,12 +356,10 @@ public function onEventDropped($eventId, $year, $month, $day)
public function getId()
{
- // Livewire 3+ has getId() on base Component class
if (method_exists(parent::class, 'getId')) {
return parent::getId();
}
- // Fallback for Livewire 2
if (!empty($this->__id)) {
return $this->__id;
}
@@ -304,6 +371,22 @@ public function getId()
return 'livewire-calendar-' . uniqid();
}
+ public function getHeaderLabelProperty(): string
+ {
+ if ($this->viewMode === 'week') {
+ $weekStart = $this->selectedDate->copy()->startOfWeek($this->weekStartsAt);
+ $weekEnd = $weekStart->copy()->endOfWeek($this->weekEndsAt);
+
+ return $weekStart->format('M j') . ' - ' . $weekEnd->format('M j');
+ }
+
+ if ($this->viewMode === 'day') {
+ return $this->selectedDate->format('D, M j');
+ }
+
+ return $this->startsAt->format('F Y');
+ }
+
/**
* @return Factory|View
* @throws Exception
@@ -311,15 +394,37 @@ public function getId()
public function render()
{
$events = $this->events();
+ $hours = $this->timeSlots();
+ $hourIndexMap = $hours->mapWithKeys(function ($value, $index) {
+ return [$value => $index];
+ });
+
+ $selectedDayEvents = $this->getEventsForDay($this->selectedDate, $events);
+ $allDayEvents = $selectedDayEvents->filter(function ($event) {
+ return !isset($event['start_time']) && !isset($event['end_time']);
+ });
+ $timedDayEvents = $selectedDayEvents->reject(function ($event) {
+ return !isset($event['start_time']) && !isset($event['end_time']);
+ })->sortBy('start_time')->values();
return view($this->calendarView)
->with([
'componentId' => $this->getId(),
'monthGrid' => $this->monthGrid(),
'events' => $events,
+ 'weekDays' => $this->weekDays(),
+ 'hours' => $hours,
+ 'hourIndexMap' => $hourIndexMap,
+ 'selectedWeekEvents' => $selectedDayEvents,
+ 'allDayEvents' => $allDayEvents,
+ 'timedDayEvents' => $timedDayEvents,
+ 'headerLabel' => $this->headerLabel,
'getEventsForDay' => function ($day) use ($events) {
return $this->getEventsForDay($day, $events);
- }
+ },
+ 'getEventsForHour' => function ($day, $hour, $eventsInput = null) use ($events) {
+ return $this->getEventsForHour($day, $hour, $eventsInput ?? $events);
+ },
]);
}
}
diff --git a/tests/LivewireCalendarTest.php b/tests/LivewireCalendarTest.php
index e199ca9..ebbef47 100755
--- a/tests/LivewireCalendarTest.php
+++ b/tests/LivewireCalendarTest.php
@@ -301,4 +301,35 @@ public function test_custom_drag_and_drop_classes()
$this->assertEquals('bg-blue-100', $component->get('dragAndDropClasses'));
}
+
+ public function test_view_mode_defaults_to_month()
+ {
+ $component = $this->createComponent([]);
+
+ $this->assertEquals('month', $component->get('viewMode'));
+ }
+
+ public function test_can_switch_view_modes()
+ {
+ $component = $this->createComponent([]);
+
+ $component->call('setViewMode', 'week');
+ $this->assertEquals('week', $component->get('viewMode'));
+
+ $component->call('setViewMode', 'day');
+ $this->assertEquals('day', $component->get('viewMode'));
+ }
+
+ public function test_can_navigate_week_and_day()
+ {
+ $component = $this->createComponent([]);
+
+ $selectedDate = Carbon::parse($component->get('selectedDate'));
+
+ $component->call('goToNextWeek');
+ $this->assertTrue(Carbon::parse($component->get('selectedDate'))->isSameDay($selectedDate->copy()->addWeek()));
+
+ $component->call('goToPreviousDay');
+ $this->assertTrue(Carbon::parse($component->get('selectedDate'))->isSameDay($selectedDate->copy()->addDays(6)));
+ }
}
From 8f3ace9c8b5209b1d5f781bc18686380e27e5d6c Mon Sep 17 00:00:00 2001
From: Cassian Andor <259291226+andorcassian@users.noreply.github.com>
Date: Tue, 17 Feb 2026 22:16:03 -0800
Subject: [PATCH 2/2] Add before/after viewport screenshot artifacts for PR
---
docs/screenshots/phase2/after-desktop-1280.png | Bin 0 -> 449 bytes
docs/screenshots/phase2/after-mobile-375.png | Bin 0 -> 361 bytes
docs/screenshots/phase2/before-desktop-1280.png | Bin 0 -> 449 bytes
docs/screenshots/phase2/before-mobile-375.png | Bin 0 -> 361 bytes
4 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 docs/screenshots/phase2/after-desktop-1280.png
create mode 100644 docs/screenshots/phase2/after-mobile-375.png
create mode 100644 docs/screenshots/phase2/before-desktop-1280.png
create mode 100644 docs/screenshots/phase2/before-mobile-375.png
diff --git a/docs/screenshots/phase2/after-desktop-1280.png b/docs/screenshots/phase2/after-desktop-1280.png
new file mode 100644
index 0000000000000000000000000000000000000000..0193abc0294c6ab3915df3c1fdd0c12bc02f2ee7
GIT binary patch
literal 449
zcmeAS@N?(olHy`uVBq!ia0y~yU
=ES4z)+>ez|hdb!0-zw
z)bN6Vq11qZ;Z*_ygVhWM2JwP9y8>;15^MoJA+GPg{Re{RT~phDB8*Ai?k~
zIqW5#zOL-An1tB)%?}jMb^;2imbgZgq$HN4S|t~y0x1R~10yqC10!8Six2}dD??K&
zQ&VjN11kfAnC}Q!>*kacdA-ZL<=nK?80>NoH!C8<`)MX5lF!N|bKOxM6j*U%!wz|6|f
z)XLOU+rYrez##csizteQ-29Zxv`X9>gjUxvULR>*?y}vd$@?2>{u^S-JoK
literal 0
HcmV?d00001
diff --git a/docs/screenshots/phase2/before-desktop-1280.png b/docs/screenshots/phase2/before-desktop-1280.png
new file mode 100644
index 0000000000000000000000000000000000000000..1cbd9f823838158d0726fce59832dab346da10d7
GIT binary patch
literal 449
zcmeAS@N?(olHy`uVBq!ia0y~yU=ES4z)+>ez|hdb!0-zw
z)bN6Vq11qZ;Z*_ygVhWM2JwP9y8>;15^MoJA+A4u{rUg@|0JEKUO*AXByV>Y#{W#Z
z_kbMs5>H=O_E$_oZ2aa2if20kg;YyiBT7;dOH!?pi&B9UgOP!enXZA6uAxPUfti(|
zsg9@l-cWk5Ys
zJY5_^DsH_!XvhfU9a`|!KbJj!0gyRhpy4{hcYOxNn?axUf!yHf>gTe~DWM4f?kZV(
literal 0
HcmV?d00001
diff --git a/docs/screenshots/phase2/before-mobile-375.png b/docs/screenshots/phase2/before-mobile-375.png
new file mode 100644
index 0000000000000000000000000000000000000000..293dc5edfa1a87ef7cc28db25f32b316f77f3d2c
GIT binary patch
literal 361
zcmeAS@N?(olHy`uVBq!ia0y~yU@T`~VAf$|28v9*QLq3=DI|LY`7$t6sWC7#v@kII
z0tz*}U|=XUU|@Kaz`$TNgMmT3V9u^U8=wSRfKQ0)k6(ZO|NlQp=cyM^gfYq6-G%W#
zlkPnrhrPtp*OmPhlMoxf`GMluPCy~m64!{5l*E!$tK_0oAjM#0U}UCiV5Dnk5n^Cw
zWoT+;YNBmmU}a!%w6T97iiX_$l+3hB+#0srF_#5u(16=el9`)YT#}eufLqVwlVXQ}
zdTcyh978H@y*+5i2;?1F@YVlqKHCBylQ3v7pW(p3SpDR(Hb||ftDnm{r-UW|bB