From fa3389c25c7e5ccda9200ce292131ac75d172955 Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 16 Mar 2026 15:11:00 -0700 Subject: [PATCH 01/31] Reorganize the fields --- resources/fieldsets/event.yaml | 74 ++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/resources/fieldsets/event.yaml b/resources/fieldsets/event.yaml index 594ccb4..4993cf2 100644 --- a/resources/fieldsets/event.yaml +++ b/resources/fieldsets/event.yaml @@ -11,7 +11,7 @@ fields: monthly: Monthly every: Every multi_day: Multi-Day - width: 33 + width: 50 display: Recurrence default: none - @@ -22,14 +22,7 @@ fields: default: UTC type: dictionary display: Timezone - - - handle: all_day - field: - type: toggle - width: 33 - display: 'All Day?' - unless: - recurrence: 'equals multi_day' + width: 50 - handle: specific_days field: @@ -75,6 +68,13 @@ fields: multi_day: 'equals true' recurrence: 'equals multi_day' format: Y-m-d + - + handle: end_date_spacer + field: + type: spacer + width: 33 + if: + recurrence: 'equals none' - handle: end_date field: @@ -91,11 +91,44 @@ fields: if: recurrence: 'contains_any daily, weekly, monthly, every' format: Y-m-d + - + handle: exclude_dates + field: + type: grid + fullscreen: false + display: 'Exclude Days' + add_row: 'Add Day' + if_any: + recurrence: 'contains_any monthly, daily, weekly, every' + fields: + - + handle: date + field: + type: date + allow_blank: false + allow_time: false + require_time: false + input_format: YYYY/M/D/YYYY + display: Date + format: Y-m-d + - + handle: times_sections + field: + type: section + display: Times + - + handle: all_day + field: + type: toggle + width: 33 + display: 'All Day?' + unless: + recurrence: 'equals multi_day' - handle: start_time field: type: time - width: 25 + width: 33 display: 'Start Time' instructions: 'Input in [24-hour format](https://en.wikipedia.org/wiki/24-hour_clock)' unless_any: @@ -106,7 +139,7 @@ fields: handle: end_time field: type: time - width: 25 + width: 33 display: 'End Time' instructions: 'Input in [24-hour format](https://en.wikipedia.org/wiki/24-hour_clock)' unless_any: @@ -170,22 +203,3 @@ fields: field: 'events::event.all_day' config: width: 25 - - - handle: exclude_dates - field: - type: grid - display: 'Exclude Days' - add_row: 'Add Day' - if_any: - recurrence: 'contains_any monthly, daily, weekly, every' - fields: - - - handle: date - field: - type: date - allow_blank: false - allow_time: false - require_time: false - input_format: YYYY/M/D/YYYY - display: Date - format: Y-m-d From e36411654892b829e6e3b5bc4581177af4216e62 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 18 Mar 2026 11:34:40 -0700 Subject: [PATCH 02/31] wip --- src/ServiceProvider.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index afc41c3..7679343 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -5,14 +5,17 @@ use Statamic\Entries\Entry; use Statamic\Facades\Collection; use Statamic\Fields\Field; +use Statamic\Fields\Fields; use Statamic\Fields\Value; use Statamic\Fieldtypes\Dictionary; use Statamic\Providers\AddonServiceProvider; +use Statamic\Statamic; class ServiceProvider extends AddonServiceProvider { public function bootAddon() { + // Fields::default('events_timezone', fn () => Statamic::displayTimezone()); collect(Events::setting('collections', [['collection' => 'events']])) ->each(fn (array $collection) => Collection::computed( $collection['collection'], From 55aa50efd9410d4c0806733a3356a4448c28b3c0 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Fri, 20 Mar 2026 13:33:08 +0100 Subject: [PATCH 03/31] Add passing and failing test --- tests/EventsTest.php | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/EventsTest.php b/tests/EventsTest.php index 0d84ef7..1817cf4 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -2,6 +2,7 @@ namespace TransformStudios\Events\Tests; +use Carbon\CarbonImmutable; use Illuminate\Support\Carbon; use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Entry; @@ -386,3 +387,45 @@ expect($occurrences)->toBeEmpty(); }); + + +test('app and event in same timezone ', function () { + $startDate = CarbonImmutable::createFromDate(2026, 2, 28); + Entry::make() + ->collection('events') + ->data([ + 'start_date' => $startDate->toDateString(), + 'start_time' => '05:00', + 'end_time' => '23:00', + 'all_day' => false + ])->save(); + + $events = Events::fromCollection('events') + ->between( + CarbonImmutable::createFromDate(2026, 2, 1)->startOfDay(), + CarbonImmutable::createFromDate(2026, 2, 28)->endOfDay() + ); + + expect($events)->toHaveCount(1); +}); + +test('app and event in different timezone ', function () { + $startDate = CarbonImmutable::createFromDate(2026, 2, 28); + Entry::make() + ->collection('events') + ->data([ + 'start_date' => $startDate->toDateString(), + 'timezone' => 'America/Los_Angeles', + 'start_time' => '05:00', + 'end_time' => '23:00', + 'all_day' => false + ])->save(); + + $events = Events::fromCollection('events') + ->between( + CarbonImmutable::createFromDate(2026, 2, 1)->startOfDay(), + CarbonImmutable::createFromDate(2026, 2, 28)->endOfDay() + ); + + expect($events)->toHaveCount(1); +}); From 62cdfe6ea30f17996f8ae326e697a6e44724eaf6 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 20 Mar 2026 11:21:29 -0700 Subject: [PATCH 04/31] better test, pass test --- src/Types/SingleDayEvent.php | 25 ++++++++++++++++++++++++- tests/EventsTest.php | 15 +++++++-------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/Types/SingleDayEvent.php b/src/Types/SingleDayEvent.php index 50128d8..bfbfddd 100644 --- a/src/Types/SingleDayEvent.php +++ b/src/Types/SingleDayEvent.php @@ -4,15 +4,38 @@ use RRule\RRule; use RRule\RRuleInterface; +use RRule\RSet; class SingleDayEvent extends Event { protected function rule(): RRuleInterface { + if ($this->spansDays()) { + $rset = new RSet; + $rset->addRRule([ + 'count' => 1, + 'dtstart' => $this->start(), + 'freq' => RRule::DAILY, + ]); + $rset->addRRule([ + 'count' => 1, + 'dtstart' => $this->end(), + 'freq' => RRule::DAILY, + ]); + + return $rset; + } + return new RRule([ 'count' => 1, - 'dtstart' => $this->start()->setTimeFromTimeString($this->endTime()), + 'dtstart' => $this->end(), 'freq' => RRule::DAILY, ]); + + } + + private function spansDays(): bool + { + return $this->start()->setTimezone('UTC')->day != $this->end()->setTimezone('UTC')->day; } } diff --git a/tests/EventsTest.php b/tests/EventsTest.php index 1817cf4..04ec9c0 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -388,43 +388,42 @@ expect($occurrences)->toBeEmpty(); }); - test('app and event in same timezone ', function () { - $startDate = CarbonImmutable::createFromDate(2026, 2, 28); + $startDate = CarbonImmutable::createFromDate(2026, 2, 15); Entry::make() ->collection('events') ->data([ 'start_date' => $startDate->toDateString(), 'start_time' => '05:00', 'end_time' => '23:00', - 'all_day' => false + 'all_day' => false, ])->save(); $events = Events::fromCollection('events') ->between( CarbonImmutable::createFromDate(2026, 2, 1)->startOfDay(), - CarbonImmutable::createFromDate(2026, 2, 28)->endOfDay() + CarbonImmutable::createFromDate(2026, 2, 15)->endOfDay() ); expect($events)->toHaveCount(1); }); test('app and event in different timezone ', function () { - $startDate = CarbonImmutable::createFromDate(2026, 2, 28); + $startDate = CarbonImmutable::createFromDate(2026, 2, 15); Entry::make() ->collection('events') ->data([ 'start_date' => $startDate->toDateString(), 'timezone' => 'America/Los_Angeles', 'start_time' => '05:00', - 'end_time' => '23:00', - 'all_day' => false + 'end_time' => '16:00', + 'all_day' => false, ])->save(); $events = Events::fromCollection('events') ->between( CarbonImmutable::createFromDate(2026, 2, 1)->startOfDay(), - CarbonImmutable::createFromDate(2026, 2, 28)->endOfDay() + CarbonImmutable::createFromDate(2026, 2, 15)->endOfDay() ); expect($events)->toHaveCount(1); From 74d4dd88255f9284b29bd15f224d67e716c9c93e Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 20 Mar 2026 11:21:36 -0700 Subject: [PATCH 05/31] small tidy --- src/Events.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Events.php b/src/Events.php index d05c350..8950d88 100644 --- a/src/Events.php +++ b/src/Events.php @@ -200,7 +200,7 @@ private function isMultiDay(Entry $occurrence): bool private function occurrences(callable $generator): EntryCollection { return $this->entries - ->filter(fn (Entry $occurrence) => $this->hasStartDate($occurrence)) + ->filter(fn (Entry $event) => $this->hasStartDate($event)) // take each event and generate the occurrences ->flatMap(callback: $generator) ->reject(fn (Entry $occurrence) => collect($occurrence->exclude_dates) From 052971ed4b0c593a162b74a3b12b3b0f7121070f Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 20 Mar 2026 11:42:36 -0700 Subject: [PATCH 06/31] Tidy --- src/Types/SingleDayEvent.php | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Types/SingleDayEvent.php b/src/Types/SingleDayEvent.php index bfbfddd..48c2318 100644 --- a/src/Types/SingleDayEvent.php +++ b/src/Types/SingleDayEvent.php @@ -10,28 +10,22 @@ class SingleDayEvent extends Event { protected function rule(): RRuleInterface { + $rset = tap(new RSet)->addRRule([ + 'count' => 1, + 'dtstart' => $this->end(), + 'freq' => RRule::DAILY, + ]); + + // if the occurrence spans days, include the start so that it's picked up on the "between" method if ($this->spansDays()) { - $rset = new RSet; $rset->addRRule([ 'count' => 1, 'dtstart' => $this->start(), 'freq' => RRule::DAILY, ]); - $rset->addRRule([ - 'count' => 1, - 'dtstart' => $this->end(), - 'freq' => RRule::DAILY, - ]); - - return $rset; } - return new RRule([ - 'count' => 1, - 'dtstart' => $this->end(), - 'freq' => RRule::DAILY, - ]); - + return $rset; } private function spansDays(): bool From 1ac50fadaaf9ec4b3afb76c0b3e355d0115b5904 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 20 Mar 2026 12:56:40 -0700 Subject: [PATCH 07/31] Tests --- tests/EventsTest.php | 20 --- tests/Types/RecurringDailyEventsTest.php | 169 +++------------------- tests/Types/RecurringEveryXEventsTest.php | 117 --------------- 3 files changed, 22 insertions(+), 284 deletions(-) diff --git a/tests/EventsTest.php b/tests/EventsTest.php index 04ec9c0..cea5da6 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -388,26 +388,6 @@ expect($occurrences)->toBeEmpty(); }); -test('app and event in same timezone ', function () { - $startDate = CarbonImmutable::createFromDate(2026, 2, 15); - Entry::make() - ->collection('events') - ->data([ - 'start_date' => $startDate->toDateString(), - 'start_time' => '05:00', - 'end_time' => '23:00', - 'all_day' => false, - ])->save(); - - $events = Events::fromCollection('events') - ->between( - CarbonImmutable::createFromDate(2026, 2, 1)->startOfDay(), - CarbonImmutable::createFromDate(2026, 2, 15)->endOfDay() - ); - - expect($events)->toHaveCount(1); -}); - test('app and event in different timezone ', function () { $startDate = CarbonImmutable::createFromDate(2026, 2, 15); Entry::make() diff --git a/tests/Types/RecurringDailyEventsTest.php b/tests/Types/RecurringDailyEventsTest.php index daf09f0..ac7c9b0 100755 --- a/tests/Types/RecurringDailyEventsTest.php +++ b/tests/Types/RecurringDailyEventsTest.php @@ -6,6 +6,7 @@ use Carbon\CarbonImmutable; use Statamic\Facades\Entry; use TransformStudios\Events\EventFactory; +use TransformStudios\Events\Events; test('null next date if now after end date', function () { $recurringEntry = Entry::make() @@ -68,150 +69,24 @@ expect($nextOccurrences[0]->start)->toEqual($startDate); }); -// public function test_can_generate_next_day_if_after() -// { -// $startDate = CarbonImmutable::now()->setTimeFromTimeString('11:00:00'); -// $event = [ -// 'start_date' => $startDate->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'recurrence' => 'daily', -// ]; -// Carbon::setTestNow($startDate->addMinute()); -// $event = EventFactory::createFromArray($event); -// $nextOccurrences = $event->nextOccurrences(1); -// $this->assertEquals($startDate->addDay(), $nextDate->start()); -// } -// public function test_can_generate_next_x_dates_from_today_before_event_time() -// { -// $startDate = Carbon::now()->setTimeFromTimeString('11:00:00'); -// $event = EventFactory::createFromArray( -// [ -// 'start_date' => $startDate->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'recurrence' => 'daily', -// ] -// ); -// for ($x = 0; $x < 2; $x++) { -// $events[] = $startDate->copy()->addDays($x); -// } -// $this->events->add($event); -// Carbon::setTestNow($startDate->copy()->subMinutes(1)); -// $nextDates = $this->events->upcoming(2); -// $this->assertCount(2, $nextDates); -// $this->assertEquals($events[0], $nextDates[0]->start()); -// $this->assertEquals($events[1], $nextDates[1]->start()); -// } -// public function test_can_generate_next_x_dates_from_today() -// { -// $startDate = Carbon::now()->setTimeFromTimeString('11:00:00'); -// $event = EventFactory::createFromArray([ -// 'start_date' => $startDate->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'recurrence' => 'daily', -// ]); -// for ($x = 0; $x < 3; $x++) { -// $events[] = $startDate->copy()->addDays($x); -// } -// $this->events->add($event); -// Carbon::setTestNow($startDate->copy()->addMinutes(1)); -// $nextDates = $this->events->upcoming(3); -// $this->assertCount(3, $nextDates); -// $this->assertEquals($events[0], $nextDates[0]->start()); -// $this->assertEquals($events[1], $nextDates[1]->start()); -// $this->assertEquals($events[2], $nextDates[2]->start()); -// } -// public function test_generates_all_occurrences_when_daily_after_start_date() -// { -// $startDate = Carbon::now()->setTimeFromTimeString('11:00:00'); -// $event = EventFactory::createFromArray( -// [ -// 'start_date' => $startDate->copy()->addDay()->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'end_date' => $startDate->copy()->addDays(3)->toDateString(), -// 'recurrence' => 'daily', -// ] -// ); -// for ($x = 2; $x <= 3; $x++) { -// $events[] = $startDate->copy()->addDays($x); -// } -// $this->events->add($event); -// Carbon::setTestNow($startDate->copy()->addDays(1)->addHour(1)); -// $nextEvents = $this->events->upcoming(3); -// $this->assertCount(2, $nextEvents); -// $this->assertEquals($events[0], $nextEvents[0]->start()); -// $this->assertEquals($events[1], $nextEvents[1]->start()); -// } -// public function test_can_get_last_day_when_before() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// 'end_date' => Carbon::now()->addDays(7)->toDateString(), -// ])); -// $from = Carbon::now()->addDays(7); -// $to = Carbon::now()->endOfDay()->addDays(10); -// $events = $this->events->all($from, $to); -// $this->assertCount(1, $events); -// } -// public function test_generates_all_daily_occurrences_single_event_from_to() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// 'end_date' => Carbon::now()->addDays(7)->toDateString(), -// ])); -// $from = Carbon::now()->subDays(1); -// $to = Carbon::now()->endOfDay()->addDays(10); -// $events = $this->events->all($from, $to); -// $this->assertCount(8, $events); -// } -// public function test_generates_all_daily_occurrences_single_event_from_to_without_end_date() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// ])); -// $from = Carbon::now()->subDays(1); -// $to = Carbon::now()->endOfDay()->addDays(10); -// $events = $this->events->all($from, $to); -// $this->assertCount(11, $events); -// } -// public function test_can_exclude_dates() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// 'except' => [ -// ['date' => Carbon::now()->addDays(2)->toDateString()], -// ['date' => Carbon::now()->addDays(4)->toDateString()], -// ], -// ])); -// $from = Carbon::now()->subDays(1); -// $to = Carbon::now()->endOfDay()->addDays(5); -// $events = $this->events->all($from, $to)->toArray(); -// $this->assertCount(4, $events); -// $this->assertEquals(Carbon::now()->toDateString(), $events[0]['start_date']); -// $this->assertEquals(Carbon::now()->addDays(1)->toDateString(), $events[1]['start_date']); -// $this->assertEquals(Carbon::now()->addDays(3)->toDateString(), $events[2]['start_date']); -// $this->assertEquals(Carbon::now()->addDays(5)->toDateString(), $events[3]['start_date']); -// } +test('app and event in different timezone ', function () { + $startDate = CarbonImmutable::createFromDate(2026, 2, 15); + Entry::make() + ->collection('events') + ->data([ + 'start_date' => $startDate->toDateString(), + 'timezone' => 'America/Los_Angeles', + 'start_time' => '05:00', + 'end_time' => '16:00', + 'recurrence' => 'monthly', + 'specific_days' => ['third_monday'], + ])->save(); + + $events = Events::fromCollection('events') + ->between( + CarbonImmutable::createFromDate(2026, 2, 15)->startOfDay(), + CarbonImmutable::createFromDate(2026, 3, 16)->endOfDay() + ); + + expect($events)->toHaveCount(2); +}); diff --git a/tests/Types/RecurringEveryXEventsTest.php b/tests/Types/RecurringEveryXEventsTest.php index 1079266..87f52d6 100755 --- a/tests/Types/RecurringEveryXEventsTest.php +++ b/tests/Types/RecurringEveryXEventsTest.php @@ -245,120 +245,3 @@ expect($occurrences[0]->start)->toEqual($events[0]); expect($occurrences[1]->start)->toEqual($events[1]); }); - -/* - public function test_can_get_last_day_when_before() - { - Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); - - $event = [ - 'id' => 'daily-event', - 'start_date' => Carbon::now()->toDateString(), - 'start_time' => '13:00', - 'end_time' => '15:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'days', - 'end_date' => Carbon::now()->addDays(8)->toDateString(), - ]; - - $this->events->add(EventFactory::createFromArray($event)); - - $from = Carbon::now()->addDays(7); - $to = Carbon::now()->endOfDay()->addDays(10); - - $events = $this->events->all($from, $to); - - $this->assertCount(1, $events); - - $event['start_date'] = Carbon::now()->addDays(8)->toDateString(); - - $this->assertEquals($event, $events[0]->toArray()); - } - - public function test_generates_all_daily_occurrences_single_event_from_to_with_end_date() - { - Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); - - $this->events->add(EventFactory::createFromArray( - [ - 'id' => 'daily-event', - 'start_date' => Carbon::now()->toDateString(), - 'start_time' => '13:00', - 'end_time' => '15:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'days', - 'end_date' => Carbon::now()->addDays(8)->toDateString(), - ] - )); - - $from = Carbon::now()->subDays(1); - $to = Carbon::now()->endOfDay()->addDays(10); - - $events = $this->events->all($from, $to); - - $this->assertCount(5, $events); - } - - public function test_generates_all_daily_occurrences_single_event_from_to_without_end_date() - { - Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); - - $this->events->add(EventFactory::createFromArray( - [ - 'id' => 'daily-event', - 'start_date' => Carbon::now()->toDateString(), - 'start_time' => '13:00', - 'end_time' => '15:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'days', - ] - )); - - $from = Carbon::now()->subDays(1); - $to = Carbon::now()->endOfDay()->addDays(10); - - $events = $this->events->all($from, $to); - - $this->assertCount(6, $events); - } - - public function test_can_generate_next_x_weeks_if_in_different_weeks() - { - $event = EventFactory::createFromArray( - [ - 'start_date' => '2020-01-03', - 'start_time' => '11:00', - 'end_time' => '12:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'weeks', - ] - ); - - $day = $event->upcomingDate(Carbon::parse('2021-01-31')); - - $this->assertNotNull($day); - $this->assertEquals('2021-02-12', $day->startDate()); - } - - public function test_returns_null_when_dates_between_dont_have_event() - { - $event = EventFactory::createFromArray( - [ - 'start_date' => '2021-01-29', - 'start_time' => '11:00', - 'end_time' => '12:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'weeks', - ] - ); - - $dates = $event->datesBetween('2021-02-18', '2021-02-19'); - - $this->assertEmpty($dates); - } -*/ From bea96565255c014f076f94805314d11ece64c1a3 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 20 Mar 2026 12:56:51 -0700 Subject: [PATCH 08/31] could be usefule on the front end --- src/Types/Event.php | 5 +++++ src/Types/SingleDayEvent.php | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Types/Event.php b/src/Types/Event.php index 8728ae6..e1893fc 100644 --- a/src/Types/Event.php +++ b/src/Types/Event.php @@ -79,6 +79,11 @@ public function nextOccurrences(int $limit = 1): Collection return $this->collect($this->rule()->getOccurrencesAfter(date: now(), inclusive: true, limit: $limit)); } + public function spansDays(): bool + { + return $this->start()->setTimezone(config('app.timezone'))->day != $this->end()->setTimezone(config('app.timezone'))->day; + } + public function startTime(): string { return $this->start_time ?? now()->startOfDay()->toTimeString('second'); diff --git a/src/Types/SingleDayEvent.php b/src/Types/SingleDayEvent.php index 48c2318..f82a76c 100644 --- a/src/Types/SingleDayEvent.php +++ b/src/Types/SingleDayEvent.php @@ -27,9 +27,4 @@ protected function rule(): RRuleInterface return $rset; } - - private function spansDays(): bool - { - return $this->start()->setTimezone('UTC')->day != $this->end()->setTimezone('UTC')->day; - } } From 38bccf6f0bd6da8a95eeeac8e2aa323f1a33aa9f Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 23 Mar 2026 16:03:22 -0700 Subject: [PATCH 09/31] fix properly --- src/Types/Event.php | 12 +++++----- src/Types/SingleDayEvent.php | 14 +---------- tests/EventsTest.php | 36 ++++++++++++++++++++--------- tests/Types/SingleDayEventsTest.php | 18 +++++++++++++++ 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/Types/Event.php b/src/Types/Event.php index e1893fc..d443f97 100644 --- a/src/Types/Event.php +++ b/src/Types/Event.php @@ -64,7 +64,12 @@ public function isRecurring(): bool public function occurrencesBetween(string|CarbonInterface $from, string|CarbonInterface $to): Collection { - return $this->collect($this->rule()->getOccurrencesBetween(begin: $from, end: $to)); + $tz = $this->timezone['name']; + + return $this->collect($this->rule()->getOccurrencesBetween( + begin: $from->shiftTimezone($tz), + end: $to->shiftTimezone($tz) + )); } public function occursOnDate(string|CarbonInterface $date): bool @@ -79,11 +84,6 @@ public function nextOccurrences(int $limit = 1): Collection return $this->collect($this->rule()->getOccurrencesAfter(date: now(), inclusive: true, limit: $limit)); } - public function spansDays(): bool - { - return $this->start()->setTimezone(config('app.timezone'))->day != $this->end()->setTimezone(config('app.timezone'))->day; - } - public function startTime(): string { return $this->start_time ?? now()->startOfDay()->toTimeString('second'); diff --git a/src/Types/SingleDayEvent.php b/src/Types/SingleDayEvent.php index f82a76c..f58aff8 100644 --- a/src/Types/SingleDayEvent.php +++ b/src/Types/SingleDayEvent.php @@ -4,27 +4,15 @@ use RRule\RRule; use RRule\RRuleInterface; -use RRule\RSet; class SingleDayEvent extends Event { protected function rule(): RRuleInterface { - $rset = tap(new RSet)->addRRule([ + return new RRule([ 'count' => 1, 'dtstart' => $this->end(), 'freq' => RRule::DAILY, ]); - - // if the occurrence spans days, include the start so that it's picked up on the "between" method - if ($this->spansDays()) { - $rset->addRRule([ - 'count' => 1, - 'dtstart' => $this->start(), - 'freq' => RRule::DAILY, - ]); - } - - return $rset; } } diff --git a/tests/EventsTest.php b/tests/EventsTest.php index cea5da6..dade95f 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -388,23 +388,37 @@ expect($occurrences)->toBeEmpty(); }); -test('app and event in different timezone ', function () { - $startDate = CarbonImmutable::createFromDate(2026, 2, 15); +test('app and event in same timezone ', function () { + $date = CarbonImmutable::createFromDate(2026, 2, 28); Entry::make() ->collection('events') ->data([ - 'start_date' => $startDate->toDateString(), + 'start_date' => $date->toDateString(), + 'start_time' => '05:00', + 'end_time' => '23:00', + ])->save(); + + $events1 = Events::fromCollection('events')->between($date->startOfMonth(), $date->endOfMonth()); + $events2 = Events::fromCollection('events')->between($date->startOfMonth(), $date->endOfMonth()->endOfWeek()); + + expect($events1)->toHaveCount(1); + expect($events2)->toHaveCount(1); +}); + +test('app and event in different timezone', function () { + $date = CarbonImmutable::createFromDate(2026, 2, 28); + Entry::make() + ->collection('events') + ->data([ + 'start_date' => $date->toDateString(), 'timezone' => 'America/Los_Angeles', 'start_time' => '05:00', - 'end_time' => '16:00', - 'all_day' => false, + 'end_time' => '23:00', ])->save(); - $events = Events::fromCollection('events') - ->between( - CarbonImmutable::createFromDate(2026, 2, 1)->startOfDay(), - CarbonImmutable::createFromDate(2026, 2, 15)->endOfDay() - ); + $events1 = Events::fromCollection('events')->between($date->startOfMonth(), $date->endOfMonth()); + $events2 = Events::fromCollection('events')->between($date->startOfMonth(), $date->endOfMonth()->endOfWeek()); - expect($events)->toHaveCount(1); + expect($events1)->toHaveCount(1); + expect($events2)->toHaveCount(1); }); diff --git a/tests/Types/SingleDayEventsTest.php b/tests/Types/SingleDayEventsTest.php index 867f563..66c7d0b 100755 --- a/tests/Types/SingleDayEventsTest.php +++ b/tests/Types/SingleDayEventsTest.php @@ -144,3 +144,21 @@ expect($nextOccurrences[0]->has_end_time)->toBeFalse(); }); + +test('app and event in different timezone ', function () { + $date = CarbonImmutable::createFromDate(2026, 2, 28); + $entry = Entry::make() + ->collection('events') + ->data([ + 'start_date' => $date->toDateString(), + 'timezone' => 'America/Los_Angeles', + 'start_time' => '05:00', + 'end_time' => '23:00', + ]); + + $events1 = EventFactory::createFromEntry($entry)->occurrencesBetween($date->startOfMonth(), $date->endOfMonth()); + $events2 = EventFactory::createFromEntry($entry)->occurrencesBetween($date->startOfMonth(), $date->endOfMonth()->endOfWeek()); + + expect($events1)->toHaveCount(1); + expect($events2)->toHaveCount(1); +}); From 68af710d163b4387a900428e9b4cfb1c8814dffe Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 23 Mar 2026 16:03:37 -0700 Subject: [PATCH 10/31] can list weekdays --- src/Tags/Events.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Tags/Events.php b/src/Tags/Events.php index 277c407..7a244b9 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -5,6 +5,7 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Carbon\CarbonInterface; +use Carbon\CarbonPeriod; use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; use Statamic\Contracts\Query\Builder; @@ -47,6 +48,16 @@ public function calendar(): Collection return $this->output($this->makeEmptyDates(from: $from, to: $to)->merge($occurrences)->values()); } + public function daysOfWeek(): Collection + { + return collect(CarbonPeriod::dates(now()->startOfWeek(), now()->endOfWeek())) + ->map(fn (Carbon $date) => [ + 'short' => $date->format('D')[0], + 'medium' => $date->format('D'), + 'long' => $date->format('l'), + ]); + } + public function downloadLink(): string { return route( From bbdcef1dcc611db3c1c22a9c8273cff41d5e7b4c Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 25 Mar 2026 12:29:58 -0700 Subject: [PATCH 11/31] use site locale --- src/Modifiers/IsEndOfWeek.php | 14 ++++++++++++-- src/Modifiers/IsStartOfWeek.php | 14 ++++++++++++-- src/Tags/Events.php | 14 +++++++++++++- tests/Modifiers/IsStartOfWeekTest.php | 26 ++++++++++++++++++++++++++ tests/Tags/EventsTest.php | 22 ++++++++++++++++++++++ tests/TestCase.php | 9 --------- 6 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 tests/Modifiers/IsStartOfWeekTest.php diff --git a/src/Modifiers/IsEndOfWeek.php b/src/Modifiers/IsEndOfWeek.php index ec09b4e..b7b9301 100755 --- a/src/Modifiers/IsEndOfWeek.php +++ b/src/Modifiers/IsEndOfWeek.php @@ -3,16 +3,26 @@ namespace TransformStudios\Events\Modifiers; use Carbon\CarbonImmutable; +use Statamic\Facades\Site; use Statamic\Modifiers\Modifier; class IsEndOfWeek extends Modifier { public function index($value, $params, $context) { + /* + have to do this because Statamic sets the Carbon locale + to the `lang` of the site, instead of the `locale` + */ + $currentLocale = CarbonImmutable::getLocale(); + CarbonImmutable::setLocale(Site::current()->locale()); + $date = CarbonImmutable::parse($value); - $date->isSameDay($date->locale(CarbonImmutable::getLocale())->startOfWeek()); + $isStartOfWeek = $date->dayOfWeek == now()->endOfWeek()->dayOfWeek; + + CarbonImmutable::setLocale($currentLocale); - return $date->dayOfWeek == now()->endOfWeek()->dayOfWeek; + return $isStartOfWeek; } } diff --git a/src/Modifiers/IsStartOfWeek.php b/src/Modifiers/IsStartOfWeek.php index e011221..b300bf7 100755 --- a/src/Modifiers/IsStartOfWeek.php +++ b/src/Modifiers/IsStartOfWeek.php @@ -3,16 +3,26 @@ namespace TransformStudios\Events\Modifiers; use Carbon\CarbonImmutable; +use Statamic\Facades\Site; use Statamic\Modifiers\Modifier; class IsStartOfWeek extends Modifier { public function index($value, $params, $context) { + /* + have to do this because Statamic sets the Carbon locale + to the `lang` of the site, instead of the `locale` + */ + $currentLocale = CarbonImmutable::getLocale(); + CarbonImmutable::setLocale(Site::current()->locale()); + $date = CarbonImmutable::parse($value); - $date->isSameDay($date->locale(CarbonImmutable::getLocale())->startOfWeek()); + $isStartOfWeek = $date->dayOfWeek == now()->startOfWeek()->dayOfWeek; + + CarbonImmutable::setLocale($currentLocale); - return $date->dayOfWeek == now()->startOfWeek()->dayOfWeek; + return $isStartOfWeek; } } diff --git a/src/Tags/Events.php b/src/Tags/Events.php index 7a244b9..52af70e 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -13,6 +13,7 @@ use Statamic\Entries\Entry; use Statamic\Entries\EntryCollection; use Statamic\Facades\Compare; +use Statamic\Facades\Site; use Statamic\Support\Arr; use Statamic\Support\Str; use Statamic\Tags\Concerns\OutputsItems; @@ -50,12 +51,23 @@ public function calendar(): Collection public function daysOfWeek(): Collection { - return collect(CarbonPeriod::dates(now()->startOfWeek(), now()->endOfWeek())) + /* + have to do this because Statamic sets the Carbon locale + to the `lang` of the site, instead of the `locale` + */ + $currentLocale = Carbon::getLocale(); + Carbon::setLocale(Site::current()->locale()); + + $days = collect(CarbonPeriod::dates(now()->startOfWeek(), now()->endOfWeek())) ->map(fn (Carbon $date) => [ 'short' => $date->format('D')[0], 'medium' => $date->format('D'), 'long' => $date->format('l'), ]); + + Carbon::setLocale($currentLocale); + + return $days; } public function downloadLink(): string diff --git a/tests/Modifiers/IsStartOfWeekTest.php b/tests/Modifiers/IsStartOfWeekTest.php new file mode 100644 index 0000000..0a28048 --- /dev/null +++ b/tests/Modifiers/IsStartOfWeekTest.php @@ -0,0 +1,26 @@ +andReturn(new Site('default', [ + 'name' => 'Laravel', + 'url' => '/', + 'locale' => 'en_US', + 'lang' => 'en', + ], true)); + + Carbon::setTestNow('2026-3-25 12:00pm'); + + $modified = modify('2026-3-22'); + expect($modified)->toBe(true); +}); + +function modify(string $value) +{ + return Modify::value($value)->isStartOfWeek()->fetch(); +} diff --git a/tests/Tags/EventsTest.php b/tests/Tags/EventsTest.php index ad53291..a36301d 100755 --- a/tests/Tags/EventsTest.php +++ b/tests/Tags/EventsTest.php @@ -5,6 +5,8 @@ use Illuminate\Support\Carbon; use Statamic\Facades\Cascade; use Statamic\Facades\Entry; +use Statamic\Facades\Site as SiteFacade; +use Statamic\Sites\Site; use Statamic\Support\Arr; use TransformStudios\Events\Tags\Events; @@ -387,3 +389,23 @@ expect($this->tag->today())->toHaveCount(0); }); + +it('sets the correct short form of the days of week', function () { + expect($this->tag->daysOfWeek())->pluck('short')->toBe(['M', 'Tu', 'W', 'Th', 'F', 'Sa', 'Su']); +}); + +it('uses the current site locale to get days of week', function () { + SiteFacade::shouldReceive('current') + ->andReturn(new Site('default', [ + 'name' => 'Laravel', + 'url' => '/', + 'locale' => 'en_US', + 'lang' => 'en', + ], true)); + + expect($this->tag->daysOfWeek())->first()->toBe([ + 'short' => 'S', + 'medium' => 'Sun', + 'long' => 'Sunday', + ]); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index cf978cc..eb0f6d0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -28,15 +28,6 @@ abstract class TestCase extends AddonTestCase protected Blueprint $blueprint; - protected function setUp(): void - { - parent::setUp(); - - if (! file_exists($this->fakeStacheDirectory)) { - mkdir($this->fakeStacheDirectory, 0777, true); - } - } - protected function getEnvironmentSetUp($app) { parent::getEnvironmentSetUp($app); From e2dccc2132be70f52c5858c869957fc42d014d72 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 25 Mar 2026 12:34:32 -0700 Subject: [PATCH 12/31] fix test --- tests/Tags/EventsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tags/EventsTest.php b/tests/Tags/EventsTest.php index a36301d..008935f 100755 --- a/tests/Tags/EventsTest.php +++ b/tests/Tags/EventsTest.php @@ -391,7 +391,7 @@ }); it('sets the correct short form of the days of week', function () { - expect($this->tag->daysOfWeek())->pluck('short')->toBe(['M', 'Tu', 'W', 'Th', 'F', 'Sa', 'Su']); + expect($this->tag->daysOfWeek())->pluck('short')->toMatchArray(['M', 'T', 'W', 'T', 'F', 'S', 'S']); }); it('uses the current site locale to get days of week', function () { From 449cf5c6b58308763936a7003d6ae4ea64b50461 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 25 Mar 2026 13:59:06 -0700 Subject: [PATCH 13/31] move the creation to setup --- tests/TestCase.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index eb0f6d0..41d7f5e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -20,14 +20,25 @@ abstract class TestCase extends AddonTestCase protected string $addonServiceProvider = ServiceProvider::class; - protected $fakeStacheDirectory = __DIR__.'/__fixtures__/dev-null'; - protected $shouldFakeVersion = true; protected Collection $collection; protected Blueprint $blueprint; + protected function setUp(): void + { + parent::setUp(); + + Taxonomy::make('categories')->save(); + Term::make('one')->taxonomy('categories')->dataForLocale('default', [])->save(); + Term::make('two')->taxonomy('categories')->dataForLocale('default', [])->save(); + + $this->collection = CollectionFacade::make('events') + ->taxonomies(['categories']) + ->save(); + } + protected function getEnvironmentSetUp($app) { parent::getEnvironmentSetUp($app); @@ -40,13 +51,6 @@ protected function getEnvironmentSetUp($app) Fieldset::addNamespace('events', __DIR__.'/../resources/fieldsets'); app()->extend(BlueprintRepository::class, fn ($repo) => $repo->setDirectory(__DIR__.'/__fixtures__/blueprints')); - Taxonomy::make('categories')->save(); - Term::make('one')->taxonomy('categories')->dataForLocale('default', [])->save(); - Term::make('two')->taxonomy('categories')->dataForLocale('default', [])->save(); - - $this->collection = CollectionFacade::make('events') - ->taxonomies(['categories']) - ->save(); }); } } From f60ef1251ddaf3b947884d7e5b8e2fbc21b7ef79 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Thu, 26 Mar 2026 15:18:08 +0100 Subject: [PATCH 14/31] Set and restore locale for calendar generation --- src/Tags/Events.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Tags/Events.php b/src/Tags/Events.php index 52af70e..82ea4b1 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -34,6 +34,9 @@ public function between(): EntryCollection|array public function calendar(): Collection { + $currentLocale = CarbonImmutable::getLocale(); + CarbonImmutable::setLocale(Site::current()->locale()); + $month = $this->params->get('month', now()->englishMonth); $year = $this->params->get('year', now()->year); @@ -46,7 +49,11 @@ public function calendar(): Collection ->groupBy(fn (Entry $occurrence) => $occurrence->start->toDateString()) ->map(fn (EntryCollection $occurrences, string $date) => $this->day(date: $date, occurrences: $occurrences)); - return $this->output($this->makeEmptyDates(from: $from, to: $to)->merge($occurrences)->values()); + $days = $this->output($this->makeEmptyDates(from: $from, to: $to)->merge($occurrences)->values()); + + CarbonImmutable::setLocale($currentLocale); + + return $days; } public function daysOfWeek(): Collection From 62fc8f1f56d673ba0991c2de75d5414ca883e9a6 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Thu, 26 Mar 2026 16:44:50 +0100 Subject: [PATCH 15/31] add `event with timezone offset appears on the correct UTC date` test --- tests/EventsTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/EventsTest.php b/tests/EventsTest.php index dade95f..ce03a61 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -422,3 +422,21 @@ expect($events1)->toHaveCount(1); expect($events2)->toHaveCount(1); }); + +test('event with timezone offset appears on the correct UTC date', function () { + $date = CarbonImmutable::createFromDate(2026, 2, 28); + Entry::make() + ->collection('events') + ->data([ + 'start_date' => $date->toDateString(), + 'timezone' => 'America/Los_Angeles', + 'start_time' => '22:00', + 'end_time' => '23:00', + ])->save(); + + $events1 = Events::fromCollection('events')->between($date->startOfDay(), $date->endOfDay()); + $events2 = Events::fromCollection('events')->between($date->startOfDay()->addDay(), $date->endOfDay()->addDay()); + + expect($events1)->toHaveCount(0); + expect($events2)->toHaveCount(1); +}); From 329ac2ac173aeebd554394ecad106dea836b2010 Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 26 Mar 2026 12:22:18 -0700 Subject: [PATCH 16/31] =?UTF-8?q?don=E2=80=99t=20shift=20tz=20when=20query?= =?UTF-8?q?ing=20between?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Types/Event.php | 7 +------ tests/EventsTest.php | 2 +- tests/Pest.php | 6 ++++-- tests/TestCase.php | 1 - tests/Types/SingleDayEventsTest.php | 25 +++++++++++++++++++++---- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Types/Event.php b/src/Types/Event.php index d443f97..8728ae6 100644 --- a/src/Types/Event.php +++ b/src/Types/Event.php @@ -64,12 +64,7 @@ public function isRecurring(): bool public function occurrencesBetween(string|CarbonInterface $from, string|CarbonInterface $to): Collection { - $tz = $this->timezone['name']; - - return $this->collect($this->rule()->getOccurrencesBetween( - begin: $from->shiftTimezone($tz), - end: $to->shiftTimezone($tz) - )); + return $this->collect($this->rule()->getOccurrencesBetween(begin: $from, end: $to)); } public function occursOnDate(string|CarbonInterface $date): bool diff --git a/tests/EventsTest.php b/tests/EventsTest.php index ce03a61..fe1cbcd 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -421,7 +421,7 @@ expect($events1)->toHaveCount(1); expect($events2)->toHaveCount(1); -}); +})->skip(); test('event with timezone offset appears on the correct UTC date', function () { $date = CarbonImmutable::createFromDate(2026, 2, 28); diff --git a/tests/Pest.php b/tests/Pest.php index de7b364..8b301d3 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@ collection('events')->data($data); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 41d7f5e..be5d962 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -45,7 +45,6 @@ protected function getEnvironmentSetUp($app) // Assume the pro edition within tests $app['config']->set('statamic.editions.pro', true); - $app['config']->set('events.timezone', 'UTC'); Statamic::booted(function () { Fieldset::addNamespace('events', __DIR__.'/../resources/fieldsets'); diff --git a/tests/Types/SingleDayEventsTest.php b/tests/Types/SingleDayEventsTest.php index 66c7d0b..f437590 100755 --- a/tests/Types/SingleDayEventsTest.php +++ b/tests/Types/SingleDayEventsTest.php @@ -145,7 +145,25 @@ expect($nextOccurrences[0]->has_end_time)->toBeFalse(); }); -test('app and event in different timezone ', function () { +it('queries occurrences based on timezone', function () { + $utcDate = now('UTC')->setTimeFromTimeString('11:00')->toImmutable(); + $laDate = now('America/Los_Angeles')->setTimeFromTimeString('11:00')->toImmutable(); + + $entry = makeEvent([ + 'start_date' => $utcDate->toDateString(), + 'timezone' => 'America/Los_Angeles', + 'start_time' => '22:00', + 'end_time' => '23:00', + ]); + + $events1 = EventFactory::createFromEntry($entry)->occurrencesBetween($utcDate->startOfDay(), $utcDate->endOfDay()); + $events2 = EventFactory::createFromEntry($entry)->occurrencesBetween($laDate->startOfDay(), $laDate->endOfDay()); + + expect($events1)->toHaveCount(0); + expect($events2)->toHaveCount(1); +}); + +it('retrieves occurrences that span days', function () { $date = CarbonImmutable::createFromDate(2026, 2, 28); $entry = Entry::make() ->collection('events') @@ -157,8 +175,7 @@ ]); $events1 = EventFactory::createFromEntry($entry)->occurrencesBetween($date->startOfMonth(), $date->endOfMonth()); - $events2 = EventFactory::createFromEntry($entry)->occurrencesBetween($date->startOfMonth(), $date->endOfMonth()->endOfWeek()); + // $events2 = EventFactory::createFromEntry($entry)->occurrencesBetween($date->startOfMonth(), $date->endOfMonth()->endOfWeek()); expect($events1)->toHaveCount(1); - expect($events2)->toHaveCount(1); -}); +})->skip(); From 4779f9963a4e6cf699db1dbb15cb13c151e59c3b Mon Sep 17 00:00:00 2001 From: edalzell Date: Thu, 26 Mar 2026 18:11:44 -0700 Subject: [PATCH 17/31] get all tests passing --- src/Types/Event.php | 4 ++-- src/Types/MultiDayEvent.php | 11 +---------- src/Types/RecurringEvent.php | 2 +- src/Types/SingleDayEvent.php | 15 ++++++++++++--- tests/EventsTest.php | 2 +- tests/Types/SingleDayEventsTest.php | 26 +++++++++++++------------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Types/Event.php b/src/Types/Event.php index 8728ae6..f340dac 100644 --- a/src/Types/Event.php +++ b/src/Types/Event.php @@ -14,7 +14,7 @@ abstract class Event { - abstract protected function rule(): RRuleInterface; + abstract protected function rule(bool $useEnd = false): RRuleInterface; public function __construct(protected Entry $event) {} @@ -76,7 +76,7 @@ public function occursOnDate(string|CarbonInterface $date): bool public function nextOccurrences(int $limit = 1): Collection { - return $this->collect($this->rule()->getOccurrencesAfter(date: now(), inclusive: true, limit: $limit)); + return $this->collect($this->rule(true)->getOccurrencesAfter(date: now(), inclusive: true, limit: $limit)); } public function startTime(): string diff --git a/src/Types/MultiDayEvent.php b/src/Types/MultiDayEvent.php index 78177bc..c108876 100644 --- a/src/Types/MultiDayEvent.php +++ b/src/Types/MultiDayEvent.php @@ -93,17 +93,8 @@ public function toICalendarEvents(): array ->all(); } - protected function rule(bool $collapseDays = false): RRuleInterface + protected function rule(bool $useEnd = false): RRuleInterface { - // if we're collapsing, then return an rrule instead of rset and use start of first day to end of last day - if ($this->collapseMultiDays) { - return new RRule([ - 'count' => 1, - 'dtstart' => $this->end(), - 'freq' => RRule::DAILY, - ]); - } - return tap( new RSet, fn (RSet $rset) => $this->days->each(fn (Day $day) => $rset->addRRule([ diff --git a/src/Types/RecurringEvent.php b/src/Types/RecurringEvent.php index 773623a..2b5723a 100644 --- a/src/Types/RecurringEvent.php +++ b/src/Types/RecurringEvent.php @@ -52,7 +52,7 @@ public function toICalendarEvents(): array return [$iCalEvent]; } - protected function rule(): RRuleInterface + protected function rule(bool $useEnd = false): RRuleInterface { $rule = [ 'dtstart' => $this->end(), diff --git a/src/Types/SingleDayEvent.php b/src/Types/SingleDayEvent.php index f58aff8..af39d16 100644 --- a/src/Types/SingleDayEvent.php +++ b/src/Types/SingleDayEvent.php @@ -7,11 +7,20 @@ class SingleDayEvent extends Event { - protected function rule(): RRuleInterface + protected function rule(bool $useEnd = false): RRuleInterface { + if ($useEnd) { + return new RRule([ + 'count' => 1, + 'dtstart' => $this->end(), + 'freq' => RRule::DAILY, + ]); + } + return new RRule([ - 'count' => 1, - 'dtstart' => $this->end(), + // 'count' => 1, + 'dtstart' => $this->start(), + 'until' => $this->end(), 'freq' => RRule::DAILY, ]); } diff --git a/tests/EventsTest.php b/tests/EventsTest.php index fe1cbcd..ce03a61 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -421,7 +421,7 @@ expect($events1)->toHaveCount(1); expect($events2)->toHaveCount(1); -})->skip(); +}); test('event with timezone offset appears on the correct UTC date', function () { $date = CarbonImmutable::createFromDate(2026, 2, 28); diff --git a/tests/Types/SingleDayEventsTest.php b/tests/Types/SingleDayEventsTest.php index f437590..3c2032f 100755 --- a/tests/Types/SingleDayEventsTest.php +++ b/tests/Types/SingleDayEventsTest.php @@ -5,6 +5,7 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Carbon\CarbonTimeZone; +use Illuminate\Support\Facades\Date; use Statamic\Facades\Entry; use TransformStudios\Events\EventFactory; use TransformStudios\Events\Types\SingleDayEvent; @@ -146,36 +147,35 @@ }); it('queries occurrences based on timezone', function () { - $utcDate = now('UTC')->setTimeFromTimeString('11:00')->toImmutable(); - $laDate = now('America/Los_Angeles')->setTimeFromTimeString('11:00')->toImmutable(); + $utcDate = Date::parse('2026-03-26 11am')->toImmutable(); + $laDate = $utcDate->shiftTimezone('America/Los_Angeles'); $entry = makeEvent([ - 'start_date' => $utcDate->toDateString(), + 'start_date' => '2026-03-26', 'timezone' => 'America/Los_Angeles', - 'start_time' => '22:00', + 'start_time' => '05:00', 'end_time' => '23:00', ]); $events1 = EventFactory::createFromEntry($entry)->occurrencesBetween($utcDate->startOfDay(), $utcDate->endOfDay()); $events2 = EventFactory::createFromEntry($entry)->occurrencesBetween($laDate->startOfDay(), $laDate->endOfDay()); - expect($events1)->toHaveCount(0); + expect($events1)->toHaveCount(1); expect($events2)->toHaveCount(1); }); -it('retrieves occurrences that span days', function () { - $date = CarbonImmutable::createFromDate(2026, 2, 28); +it('retrieves occurrences that span days in different timezone than event', function ($from, $to, $count) { $entry = Entry::make() ->collection('events') ->data([ - 'start_date' => $date->toDateString(), + 'start_date' => now()->toDateString(), 'timezone' => 'America/Los_Angeles', 'start_time' => '05:00', 'end_time' => '23:00', ]); - $events1 = EventFactory::createFromEntry($entry)->occurrencesBetween($date->startOfMonth(), $date->endOfMonth()); - // $events2 = EventFactory::createFromEntry($entry)->occurrencesBetween($date->startOfMonth(), $date->endOfMonth()->endOfWeek()); - - expect($events1)->toHaveCount(1); -})->skip(); + expect(EventFactory::createFromEntry($entry))->occurrencesBetween($from, $to)->toHaveCount($count); +})->with([ + [now()->startOfDay(), now()->endOfDay()->addDay(), 1], + [now()->startOfDay(), now()->endOfDay(), 1], +]); From d1e25edfd0d7b4b9819978cf6a45ed3e7c1809cf Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Fri, 27 Mar 2026 11:34:20 +0100 Subject: [PATCH 18/31] group occurrences by utc date and time --- src/Tags/Events.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Tags/Events.php b/src/Tags/Events.php index 82ea4b1..96de51f 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -46,7 +46,12 @@ public function calendar(): Collection $occurrences = $this ->generator() ->between(from: $from, to: $to) - ->groupBy(fn (Entry $occurrence) => $occurrence->start->toDateString()) + ->groupBy(function (Entry $occurrence) { + $start = $occurrence->start->setTimezone('UTC'); + $end = $occurrence->end->setTimezone('UTC'); + + return $start->isSameDay($end) ? $start->toDateString() : [$start->toDateString(), $end->toDateString()]; + }) ->map(fn (EntryCollection $occurrences, string $date) => $this->day(date: $date, occurrences: $occurrences)); $days = $this->output($this->makeEmptyDates(from: $from, to: $to)->merge($occurrences)->values()); From fac9f8f3f743a523e1200b17bf87d44e33d3dd00 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Fri, 27 Mar 2026 14:26:17 +0100 Subject: [PATCH 19/31] use timezones dictionary for settings --- resources/blueprints/settings.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/blueprints/settings.yaml b/resources/blueprints/settings.yaml index 453c324..4a0e3c6 100644 --- a/resources/blueprints/settings.yaml +++ b/resources/blueprints/settings.yaml @@ -27,9 +27,9 @@ tabs: - handle: timezone field: - mode: select + dictionary: timezones max_items: 1 - type: timezones + default: UTC + type: dictionary display: Timezone full_width_setting: true - default: 'UTC' From bc96e26bf1cf5e6af0a801a19bfc03a052427f57 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Fri, 27 Mar 2026 14:27:10 +0100 Subject: [PATCH 20/31] use display_timezone as fallback before app timezone --- src/Events.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Events.php b/src/Events.php index 8950d88..71d9e30 100644 --- a/src/Events.php +++ b/src/Events.php @@ -67,7 +67,7 @@ public static function setting(string $key, $default = null): mixed public static function timezone(): string { - return static::setting('timezone', config('app.timezone')); + return static::setting('timezone', config('statamic.system.display_timezone') ?? config('app.timezone')); } private function __construct() {} From 53460fc50728e4e72218f3cabef88c7e19653818 Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Fri, 27 Mar 2026 14:28:21 +0100 Subject: [PATCH 21/31] group occurrences based on the default timezone --- src/Tags/Events.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tags/Events.php b/src/Tags/Events.php index 96de51f..ea32d57 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -40,19 +40,19 @@ public function calendar(): Collection $month = $this->params->get('month', now()->englishMonth); $year = $this->params->get('year', now()->year); - $from = parse_date($month.' '.$year)->startOfMonth()->startOfWeek(); - $to = parse_date($month.' '.$year)->endOfMonth()->endOfWeek(); + $from = parse_date($month . ' ' . $year)->startOfMonth()->startOfWeek(); + $to = parse_date($month . ' ' . $year)->endOfMonth()->endOfWeek(); $occurrences = $this ->generator() ->between(from: $from, to: $to) ->groupBy(function (Entry $occurrence) { - $start = $occurrence->start->setTimezone('UTC'); - $end = $occurrence->end->setTimezone('UTC'); + $start = $occurrence->start->setTimezone($this->params->get('timezone') ?? Generator::timezone()); + $end = $occurrence->end->setTimezone($this->params->get('timezone') ?? Generator::timezone()); return $start->isSameDay($end) ? $start->toDateString() : [$start->toDateString(), $end->toDateString()]; }) - ->map(fn (EntryCollection $occurrences, string $date) => $this->day(date: $date, occurrences: $occurrences)); + ->map(fn(EntryCollection $occurrences, string $date) => $this->day(date: $date, occurrences: $occurrences)); $days = $this->output($this->makeEmptyDates(from: $from, to: $to)->merge($occurrences)->values()); From 508c957e0444be0ffdec955f1980b22fff1730cb Mon Sep 17 00:00:00 2001 From: Marco Rieser Date: Fri, 27 Mar 2026 15:41:04 +0100 Subject: [PATCH 22/31] use period to match all spanning days --- src/Tags/Events.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Tags/Events.php b/src/Tags/Events.php index ea32d57..3bfc413 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -6,6 +6,7 @@ use Carbon\CarbonImmutable; use Carbon\CarbonInterface; use Carbon\CarbonPeriod; +use Carbon\CarbonPeriodImmutable; use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; use Statamic\Contracts\Query\Builder; @@ -47,10 +48,14 @@ public function calendar(): Collection ->generator() ->between(from: $from, to: $to) ->groupBy(function (Entry $occurrence) { - $start = $occurrence->start->setTimezone($this->params->get('timezone') ?? Generator::timezone()); - $end = $occurrence->end->setTimezone($this->params->get('timezone') ?? Generator::timezone()); - - return $start->isSameDay($end) ? $start->toDateString() : [$start->toDateString(), $end->toDateString()]; + $periodInTimezone = CarbonPeriodImmutable::between( + $occurrence->start->setTimezone($this->params->get('timezone') ?? Generator::timezone())->startOfDay(), + $occurrence->end->setTimezone($this->params->get('timezone') ?? Generator::timezone())->endOfDay() + ); + + return collect($periodInTimezone->toArray()) + ->map(fn (CarbonImmutable $date) => $date->toDateString()) + ->all(); }) ->map(fn(EntryCollection $occurrences, string $date) => $this->day(date: $date, occurrences: $occurrences)); From 43134383248a006adbdc9fbc01f97223b115b522 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 10 Apr 2026 12:20:34 -0700 Subject: [PATCH 23/31] Rename --- src/Events.php | 11 +++++------ src/ServiceProvider.php | 2 +- src/Tags/Events.php | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Events.php b/src/Events.php index 71d9e30..8f35e4e 100644 --- a/src/Events.php +++ b/src/Events.php @@ -4,7 +4,6 @@ use Carbon\CarbonInterface; use Exception; -use Illuminate\Support\Collection; use Illuminate\Support\Traits\Conditionable; use Statamic\Entries\Entry; use Statamic\Entries\EntryCollection; @@ -45,6 +44,11 @@ class Events private array $terms = []; + public static function defaultTimezone(): string + { + return static::setting('timezone', config('statamic.system.display_timezone') ?? config('app.timezone', 'UTC')); + } + public static function fromCollection(string $handle): self { return tap(new static)->collection($handle); @@ -65,11 +69,6 @@ public static function setting(string $key, $default = null): mixed return Addon::get('transformstudios/events')->settings()->get($key, $default); } - public static function timezone(): string - { - return static::setting('timezone', config('statamic.system.display_timezone') ?? config('app.timezone')); - } - private function __construct() {} public function collapseMultiDays(): self diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 7679343..0a5983d 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -26,7 +26,7 @@ public function bootAddon() private function timezone(Entry $entry, $value): string|Value { - $value ??= Events::timezone(); + $value ??= Events::defaultTimezone(); if ($entry->blueprint()->fields()->get('timezone')?->fieldtype() instanceof Dictionary) { return $value; diff --git a/src/Tags/Events.php b/src/Tags/Events.php index 3bfc413..cb146bc 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -49,8 +49,8 @@ public function calendar(): Collection ->between(from: $from, to: $to) ->groupBy(function (Entry $occurrence) { $periodInTimezone = CarbonPeriodImmutable::between( - $occurrence->start->setTimezone($this->params->get('timezone') ?? Generator::timezone())->startOfDay(), - $occurrence->end->setTimezone($this->params->get('timezone') ?? Generator::timezone())->endOfDay() + $occurrence->start->setTimezone($this->params->get('timezone') ?? Generator::defaultTimezone())->startOfDay(), + $occurrence->end->setTimezone($this->params->get('timezone') ?? Generator::defaultTimezone())->endOfDay() ); return collect($periodInTimezone->toArray()) From 245ed7769f859671c3477e86d4b7d352ceb3e908 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 10 Apr 2026 12:20:48 -0700 Subject: [PATCH 24/31] Not used --- src/Events.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Events.php b/src/Events.php index 8f35e4e..69de241 100644 --- a/src/Events.php +++ b/src/Events.php @@ -59,11 +59,6 @@ public static function fromEntry(string $id): self return tap(new static)->event($id); } - public static function collectionHandles(): Collection - { - return collect(static::setting('collections', ['events']))->keys(); - } - public static function setting(string $key, $default = null): mixed { return Addon::get('transformstudios/events')->settings()->get($key, $default); From c23a7a11763c31f9331f19f525444844abc72c01 Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 10 Apr 2026 15:42:20 -0700 Subject: [PATCH 25/31] test defaultTimezone --- resources/blueprints/settings.yaml | 1 - tests/EventsTest.php | 33 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/resources/blueprints/settings.yaml b/resources/blueprints/settings.yaml index 4a0e3c6..bda5e10 100644 --- a/resources/blueprints/settings.yaml +++ b/resources/blueprints/settings.yaml @@ -29,7 +29,6 @@ tabs: field: dictionary: timezones max_items: 1 - default: UTC type: dictionary display: Timezone full_width_setting: true diff --git a/tests/EventsTest.php b/tests/EventsTest.php index ce03a61..226cb1a 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -4,7 +4,12 @@ use Carbon\CarbonImmutable; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Config; +use Mockery; +use Statamic\Addons\Addon; +use Statamic\Addons\FileSettings; use Statamic\Extensions\Pagination\LengthAwarePaginator; +use Statamic\Facades\Addon as AddonFacade; use Statamic\Facades\Entry; use TransformStudios\Events\EventFactory; use TransformStudios\Events\Events; @@ -440,3 +445,31 @@ expect($events1)->toHaveCount(0); expect($events2)->toHaveCount(1); }); + +it('uses UTC when no app timezone set', function () { + expect(Events::defaultTimezone())->toBe('UTC'); +}); + +it('uses app timezone when no display_timezone set', function () { + Config::set('app.timezone', 'America/New_York'); + + expect(Events::defaultTimezone())->toBe('America/New_York'); +}); + +it('uses display timezone when no events timezone set', function () { + Config::set('statamic.system.display_timezone', 'America/Chicago'); + + expect(Events::defaultTimezone())->toBe('America/Chicago'); +}); + +it('uses addon setting for default timezone', function () { + $eventsAddon = Addon::make('transformstudios/events'); + $settings = new FileSettings($eventsAddon, ['timezone' => 'Australia/Adelaide']); + + $addon = Mockery::mock(); + $addon->shouldReceive('settings')->andReturn($settings); + + AddonFacade::shouldReceive('get')->with('transformstudios/events')->andReturn($addon); + + expect(Events::defaultTimezone())->toBe('Australia/Adelaide'); +}); From 8aec565cbe390579ca09c09bfc63bf81f0f90cce Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 10 Apr 2026 15:46:16 -0700 Subject: [PATCH 26/31] tidy --- tests/EventsTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/EventsTest.php b/tests/EventsTest.php index 226cb1a..422b321 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -463,11 +463,10 @@ }); it('uses addon setting for default timezone', function () { - $eventsAddon = Addon::make('transformstudios/events'); - $settings = new FileSettings($eventsAddon, ['timezone' => 'Australia/Adelaide']); - - $addon = Mockery::mock(); - $addon->shouldReceive('settings')->andReturn($settings); + $addon = tap(Mockery::mock(), fn ($mock) => $mock + ->shouldReceive('settings') + ->andReturn(new FileSettings(Addon::make('transformstudios/events'), ['timezone' => 'Australia/Adelaide'])) + ); AddonFacade::shouldReceive('get')->with('transformstudios/events')->andReturn($addon); From e72836fa4a6bab6a82c085559f1e5644591c1f0d Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 13 Apr 2026 12:20:23 -0700 Subject: [PATCH 27/31] use tag param to set timezone --- src/Events.php | 16 ++++++++++++++++ src/Tags/Events.php | 3 +++ tests/Tags/EventsTest.php | 22 ++++++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Events.php b/src/Events.php index 69de241..3f95217 100644 --- a/src/Events.php +++ b/src/Events.php @@ -44,6 +44,8 @@ class Events private array $terms = []; + private ?string $timezone = null; + public static function defaultTimezone(): string { return static::setting('timezone', config('statamic.system.display_timezone') ?? config('app.timezone', 'UTC')); @@ -139,6 +141,13 @@ public function terms(string|array $terms): self return $this; } + public function timezone(string $timezone): self + { + $this->timezone = $timezone; + + return $this; + } + public function between(string|CarbonInterface $from, string|CarbonInterface $to): EntryCollection|LengthAwarePaginator { return $this->output( @@ -157,6 +166,13 @@ private function output(callable $type): EntryCollection|LengthAwarePaginator { $occurrences = $this->entries()->occurrences(generator: $type); + if (! is_null($this->timezone)) { + $occurrences->transform(fn (Entry $occurrence) => $occurrence + ->setSupplement('start', $occurrence->start->setTimezone($this->timezone)) + ->setSupplement('end', $occurrence->end->setTimezone($this->timezone)) + ); + } + if ($this->offset) { $occurrences = $occurrences->slice(offset: $this->offset); } diff --git a/src/Tags/Events.php b/src/Tags/Events.php index cb146bc..67b38a7 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -193,6 +193,9 @@ private function generator(): Generator )->when( value: $this->params->bool('collapse_multi_days'), callback: fn (Generator $generator) => $generator->collapseMultiDays() + )->when( + value: $this->params->get('timezone'), + callback: fn (Generator $generator, string $tz) => $generator->timezone(timezone: $tz) ); } diff --git a/tests/Tags/EventsTest.php b/tests/Tags/EventsTest.php index 008935f..e51e666 100755 --- a/tests/Tags/EventsTest.php +++ b/tests/Tags/EventsTest.php @@ -8,7 +8,7 @@ use Statamic\Facades\Site as SiteFacade; use Statamic\Sites\Site; use Statamic\Support\Arr; -use TransformStudios\Events\Tags\Events; +use TransformStudios\Events\Tags\Events as EventsTag; beforeEach(function () { Entry::make() @@ -24,7 +24,7 @@ 'categories' => ['one'], ])->save(); - $this->tag = app(Events::class); + $this->tag = app(EventsTag::class); }); test('can generate between occurrences', function () { @@ -409,3 +409,21 @@ 'long' => 'Sunday', ]); }); + +test('uses the timezone param when generating occurrences', function () { + Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); + + $this->tag + ->setContext([]) + ->setParameters([ + 'collection' => 'events', + 'from' => Carbon::now()->subDay(), + 'timezone' => 'America/Vancouver', + 'to' => Carbon::now()->addDays(2), + ]); + + $occurrences = $this->tag->between(); + + expect($occurrences)->toHaveCount(1) + ->first()->start->timezone->getName()->toBe('America/Vancouver'); +}); From 53574adefddfd016486e2c74471fe86bfba59c90 Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 13 Apr 2026 13:40:13 -0700 Subject: [PATCH 28/31] Add spansday --- src/Events.php | 13 +++++++++---- tests/Tags/EventsTest.php | 31 ++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/Events.php b/src/Events.php index 3f95217..e99ecff 100644 --- a/src/Events.php +++ b/src/Events.php @@ -167,10 +167,15 @@ private function output(callable $type): EntryCollection|LengthAwarePaginator $occurrences = $this->entries()->occurrences(generator: $type); if (! is_null($this->timezone)) { - $occurrences->transform(fn (Entry $occurrence) => $occurrence - ->setSupplement('start', $occurrence->start->setTimezone($this->timezone)) - ->setSupplement('end', $occurrence->end->setTimezone($this->timezone)) - ); + $occurrences->transform(function (Entry $occurrence) { + $start = $occurrence->start->setTimezone($this->timezone); + $end = $occurrence->end->setTimezone($this->timezone); + + return $occurrence + ->setSupplement('start', $start) + ->setSupplement('end', $end) + ->setSupplement('spansDay', !$start->isSameDay($end)); + }); } if ($this->offset) { diff --git a/tests/Tags/EventsTest.php b/tests/Tags/EventsTest.php index e51e666..5cf04da 100755 --- a/tests/Tags/EventsTest.php +++ b/tests/Tags/EventsTest.php @@ -410,7 +410,7 @@ ]); }); -test('uses the timezone param when generating occurrences', function () { +it('uses the timezone param when generating occurrences', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); $this->tag @@ -427,3 +427,32 @@ expect($occurrences)->toHaveCount(1) ->first()->start->timezone->getName()->toBe('America/Vancouver'); }); + +it('sets "spansDay"', function () { + Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); + + Entry::make() + ->collection('events') + ->slug('recurring-event') + ->id('recurring-event') + ->data([ + 'title' => 'Event', + 'start_date' => Carbon::now()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '23:00', + ])->save(); + + $this->tag + ->setContext([]) + ->setParameters([ + 'collection' => 'events', + 'from' => Carbon::now()->subDay(), + 'timezone' => 'Europe/Kyiv', + 'to' => Carbon::now()->addDays(2), + ]); + + $occurrences = $this->tag->between(); + + expect($occurrences)->toHaveCount(1) + ->first()->spansDay->toBeTrue(); +}); From 1c8792acdea460593e202571e161aa9ef54381c3 Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 13 Apr 2026 15:27:15 -0700 Subject: [PATCH 29/31] refactor to use new computed defaults --- resources/blueprints/settings.yaml | 23 +++++------------ resources/fieldsets/event.yaml | 2 +- src/Events.php | 5 ++-- src/ServiceProvider.php | 40 +++++++++++------------------- 4 files changed, 24 insertions(+), 46 deletions(-) diff --git a/resources/blueprints/settings.yaml b/resources/blueprints/settings.yaml index bda5e10..997ac1a 100644 --- a/resources/blueprints/settings.yaml +++ b/resources/blueprints/settings.yaml @@ -7,23 +7,11 @@ tabs: - handle: collections field: - type: grid + type: collections display: Collections - sortable: false - add_row: 'Add Collection' - full_width_setting: true - fields: - - - handle: collection - field: - type: collections - display: Collection - width: 50 - mode: select - max_items: 1 - default: - - - collection: events + width: 50 + mode: select + default: events - handle: timezone field: @@ -31,4 +19,5 @@ tabs: max_items: 1 type: dictionary display: Timezone - full_width_setting: true + default: computed:default-events-timezone + width: 50 diff --git a/resources/fieldsets/event.yaml b/resources/fieldsets/event.yaml index 4993cf2..c7baee8 100644 --- a/resources/fieldsets/event.yaml +++ b/resources/fieldsets/event.yaml @@ -19,9 +19,9 @@ fields: field: dictionary: timezones max_items: 1 - default: UTC type: dictionary display: Timezone + default: computed:default-event-timezone width: 50 - handle: specific_days diff --git a/src/Events.php b/src/Events.php index e99ecff..dc0fa40 100644 --- a/src/Events.php +++ b/src/Events.php @@ -48,7 +48,8 @@ class Events public static function defaultTimezone(): string { - return static::setting('timezone', config('statamic.system.display_timezone') ?? config('app.timezone', 'UTC')); + // return static::setting('timezone', config('statamic.system.display_timezone') ?? config('app.timezone', 'UTC')); + return static::setting('timezone'); } public static function fromCollection(string $handle): self @@ -174,7 +175,7 @@ private function output(callable $type): EntryCollection|LengthAwarePaginator return $occurrence ->setSupplement('start', $start) ->setSupplement('end', $end) - ->setSupplement('spansDay', !$start->isSameDay($end)); + ->setSupplement('spansDay', ! $start->isSameDay($end)); }); } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 0a5983d..09e438c 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -2,12 +2,8 @@ namespace TransformStudios\Events; -use Statamic\Entries\Entry; use Statamic\Facades\Collection; -use Statamic\Fields\Field; -use Statamic\Fields\Fields; -use Statamic\Fields\Value; -use Statamic\Fieldtypes\Dictionary; +use Statamic\Facades\Field; use Statamic\Providers\AddonServiceProvider; use Statamic\Statamic; @@ -15,27 +11,19 @@ class ServiceProvider extends AddonServiceProvider { public function bootAddon() { - // Fields::default('events_timezone', fn () => Statamic::displayTimezone()); - collect(Events::setting('collections', [['collection' => 'events']])) - ->each(fn (array $collection) => Collection::computed( - $collection['collection'], - 'timezone', - $this->timezone(...) - )); - } - - private function timezone(Entry $entry, $value): string|Value - { - $value ??= Events::defaultTimezone(); - - if ($entry->blueprint()->fields()->get('timezone')?->fieldtype() instanceof Dictionary) { - return $value; - } + Field::computedDefault('default-events-timezone', fn () => Statamic::displayTimezone()); + Field::computedDefault('default-event-timezone', fn () => Events::defaultTimezone()); - return (new Field('timezone', ['type' => 'timezones', 'max_items' => 1])) - ->setValue($value) - ->setParent($entry) - ->augment() - ->value(); + collect(Events::setting('collections', ['events'])) + ->each(function (string $collection) { + Collection::findByHandle($collection)->entryBlueprint()->ensureField( + 'timezone', + [ + 'dictionary' => 'timezones', + 'max_items' => '1', + 'type' => 'dictionary', + 'default' => 'computed:default-event-timezone', + ]); + }); } } From d8338b614554b552023e976f163c7e673a9de60f Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 13 Apr 2026 15:51:11 -0700 Subject: [PATCH 30/31] handle missing collection --- src/ServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 09e438c..797ed37 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -16,7 +16,7 @@ public function bootAddon() collect(Events::setting('collections', ['events'])) ->each(function (string $collection) { - Collection::findByHandle($collection)->entryBlueprint()->ensureField( + Collection::findByHandle($collection)?->entryBlueprint()->ensureField( 'timezone', [ 'dictionary' => 'timezones', From 726166136667f826f46cf3922cfe9f6d8deaff5b Mon Sep 17 00:00:00 2001 From: edalzell Date: Mon, 13 Apr 2026 16:00:59 -0700 Subject: [PATCH 31/31] simpler --- src/UpdateScripts/ConvertConfigToSettings.php | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/UpdateScripts/ConvertConfigToSettings.php b/src/UpdateScripts/ConvertConfigToSettings.php index 334a93e..abf6e06 100644 --- a/src/UpdateScripts/ConvertConfigToSettings.php +++ b/src/UpdateScripts/ConvertConfigToSettings.php @@ -3,7 +3,6 @@ namespace TransformStudios\Events\UpdateScripts; use Illuminate\Support\Fluent; -use Illuminate\Support\Str; use Statamic\Addons\Addon; use Statamic\Addons\Settings; use Statamic\Facades\Addon as AddonFacade; @@ -56,21 +55,8 @@ private function settingsFromConfig(): ?Settings $collections = collect([$config->collection => null]) ->merge($config->collections) - ->map(function (array|string $collection, $handle) { - if (is_string($collection)) { - return [ - 'id' => Str::random(8), - 'collection' => $collection, - ]; - } - - $collectionSetting = [ - 'id' => Str::random(8), - 'collection' => $handle, - ]; - - return Arr::removeNullValues($collectionSetting); - })->values() + ->map(fn (array|string $collection, $handle) => is_string($collection) ? $collection : $handle) + ->values() ->all(); $timezone = $config->timezone;