Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 44 additions & 30 deletions resources/fieldsets/event.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fields:
monthly: Monthly
every: Every
multi_day: Multi-Day
width: 33
width: 50
display: Recurrence
default: none
-
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
5 changes: 5 additions & 0 deletions src/Types/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
16 changes: 14 additions & 2 deletions src/Types/SingleDayEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@

use RRule\RRule;
use RRule\RRuleInterface;
use RRule\RSet;

class SingleDayEvent extends Event
{
protected function rule(): RRuleInterface
{
return new RRule([
$rset = tap(new RSet)->addRRule([
'count' => 1,
'dtstart' => $this->start()->setTimeFromTimeString($this->endTime()),
'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;
}
}
22 changes: 22 additions & 0 deletions tests/EventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace TransformStudios\Events\Tests;

use Carbon\CarbonImmutable;
use Illuminate\Support\Carbon;
use Statamic\Extensions\Pagination\LengthAwarePaginator;
use Statamic\Facades\Entry;
Expand Down Expand Up @@ -386,3 +387,24 @@

expect($occurrences)->toBeEmpty();
});

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',
'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);
});
169 changes: 22 additions & 147 deletions tests/Types/RecurringDailyEventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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);
});
Loading
Loading