Skip to content
Closed
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
57 changes: 55 additions & 2 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ If you are using a different Statamic collection, update it in the addon setting

ICS downloads use the following fields if they exist:

- `address`
- `location`
- `online_url`
- `coordinates`
- `description`

Expand Down Expand Up @@ -74,6 +75,26 @@ Using the sample fieldset is the fastest way to get started.

## Fields

### Location & Online URL

`location` and `online_url` are independent and can be combined (hybrid events).

| Field | Description |
|-------|-------------|
| `location` | Free text: a plain place description or a full address |
| `online_url` | Join link for online or hybrid events (Zoom, livestream, etc.) |
| `coordinates` | Optional `latitude` / `longitude` for ICS `GEO` |

ICS mapping:

| Event | `LOCATION:` | `URL:` | `GEO:` |
|-------|-------------|--------|--------|
| Physical only | `location` | — | `coordinates` |
| Online only | `online_url` | `online_url` | — |
| Hybrid | `location` | `online_url` | `coordinates` |

A non-string `location` (for example a group field from another package) is skipped at runtime and never causes a download failure. For custom shapes, map into these handles with a [Computed Value](https://statamic.dev/content-modeling/computed-values#defining-computed-values).

### Single-Day Events

| Field | Required | Description |
Expand Down Expand Up @@ -264,5 +285,37 @@ Generates an ICS download link.

Includes:
- `location`
- `online_url`
- `coordinates`
- `description`
- `link`

---

## Upgrading to 7.0

**Back up your content before upgrading.** The 7.0 update script rewrites event entries in the collections configured in addon settings.

### Field renames

| Before | After |
|--------|-------|
| `address` | `location` |
| `link` | `online_url` |
| string `location` that is a URL | `online_url` |

`location` remains for place text. The old URL-sniffing behaviour (`location` treated as a join link when it looked like a URL) is removed.

### What the update script does automatically

- `address` → `location`
- `link` → `online_url`
- URL-valued string `location` → `online_url`
- Removes the old `address` / `link` handles after a successful migrate

### What it skips (logged with entry IDs)

- Non-string / array-shaped `location` (left completely untouched — another package may own that shape)
- Both `address` and a non-URL `location` set
- Both `link` and a URL-valued `location` set

Those ambiguous entries need a manual resolve. Computed-value mappings are not rewritten by the script.
44 changes: 44 additions & 0 deletions resources/fieldsets/event.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,47 @@ fields:
field: 'events::event.all_day'
config:
width: 25
-
handle: location_section
field:
type: section
display: Location
-
handle: location
field:
type: text
display: Location
localizable: true
instructions: 'A place description (e.g. "Outside the side exit") or a full address (e.g. "123 Main St, Surrey, BC").'
-
handle: online_url
field:
type: text
input_type: url
display: 'Online URL'
localizable: false
validate:
- nullable
- url
instructions: 'Join link for online or hybrid events (Zoom, livestream, etc.).'
-
handle: coordinates
field:
type: group
display: Coordinates
fullscreen: false
border: false
localizable: true
fields:
-
handle: latitude
field:
type: float
display: Latitude
width: 50
-
handle: longitude
field:
type: float
display: Longitude
width: 50
67 changes: 37 additions & 30 deletions src/Types/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use DateTimeInterface;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use RRule\RRuleInterface;
use Spatie\IcalendarGenerator\Components\Event as ICalendarEvent;
use Statamic\Entries\Entry;
Expand Down Expand Up @@ -107,59 +106,67 @@ public function toICalendarEvent(string|CarbonInterface $date): ?ICalendarEvent

$immutableDate = $this->toCarbonImmutable($date);

$iCalEvent = ICalendarEvent::create($this->event->title)
->withoutTimezone()
->uniqueIdentifier($this->event->id())
->startsAt($immutableDate->setTimeFromTimeString($this->startTime()))
->endsAt($immutableDate->setTimeFromTimeString($this->endTime()));
return $this->decorate(
ICalendarEvent::create($this->event->title)
->withoutTimezone()
->uniqueIdentifier($this->event->id())
->startsAt($immutableDate->setTimeFromTimeString($this->startTime()))
->endsAt($immutableDate->setTimeFromTimeString($this->endTime()))
);
}

/**
* @return ICalendarEvent[]
*/
public function toICalendarEvents(): array
{
return Arr::wrap($this->toICalendarEvent($this->start()));
}

if ($address = $this->icsAddress()) {
$iCalEvent->address($address);
protected function decorate(ICalendarEvent $iCalEvent): ICalendarEvent
{
if ($location = $this->icsLocation()) {
$iCalEvent->address($location);
}

if (! is_null($coords = $this->event->coordinates)) {
$iCalEvent->coordinates($coords['latitude'], $coords['longitude']);
if ($this->hasValidCoordinates($coords = $this->event->get('coordinates'))) {
$iCalEvent->coordinates((float) $coords['latitude'], (float) $coords['longitude']);
}

if (! is_null($description = $this->event->description)) {
if (is_string($description = $this->event->get('description')) && $description !== '') {
$iCalEvent->description($description);
}

if (! is_null($link = $this->eventUrl())) {
$iCalEvent->url($link);
if ($url = $this->icsUrl()) {
$iCalEvent->url($url);
}

return $iCalEvent;
}

/**
* @return ICalendarEvent[]
*/
public function toICalendarEvents(): array
protected function icsUrl(): ?string
{
return Arr::wrap($this->toICalendarEvent($this->start()));
$url = $this->event->get('online_url');

return is_string($url) && $url !== '' ? $url : null;
}

protected function eventUrl(): ?string
protected function icsLocation(): ?string
{
if (! is_null($link = $this->event->link)) {
return $link;
}

$location = $this->event->get('location');

if (! is_string($location)) {
return null;
if (is_string($location) && $location !== '') {
return $location;
}

return Str::isUrl($location) ? $location : null;
return $this->icsUrl();
}

protected function icsAddress(): ?string
protected function hasValidCoordinates(mixed $coords): bool
{
$address = $this->event->address ?? $this->event->get('location');

return is_string($address) && $address !== '' ? $address : null;
return is_array($coords)
&& is_numeric($coords['latitude'] ?? null)
&& is_numeric($coords['longitude'] ?? null);
}

protected function supplement(CarbonInterface $date): ?Entry
Expand Down
32 changes: 9 additions & 23 deletions src/Types/MultiDayEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,12 @@ public function toICalendarEvent(string|CarbonInterface $date): ?ICalendarEvent
$immutableDate = $this->toCarbonImmutable($date);
$day = $this->getDayFromDate($immutableDate);

$iCalEvent = ICalendarEvent::create($this->event->title)
->uniqueIdentifier($this->event->id())
->startsAt($immutableDate->setTimeFromTimeString($day->start()))
->endsAt($immutableDate->setTimeFromTimeString($day->end()));

if ($address = $this->icsAddress()) {
$iCalEvent->address($address);
}

if (! is_null($coords = $this->event->coordinates)) {
$iCalEvent->coordinates($coords['latitude'], $coords['longitude']);
}

if (! is_null($description = $this->event->description)) {
$iCalEvent->description($description);
}

if (! is_null($link = $this->eventUrl())) {
$iCalEvent->url($link);
}

return $iCalEvent;
return $this->decorate(
ICalendarEvent::create($this->event->title)
->uniqueIdentifier($this->event->id())
->startsAt($immutableDate->setTimeFromTimeString($day->start()))
->endsAt($immutableDate->setTimeFromTimeString($day->end()))
);
}

/**
Expand All @@ -89,7 +73,9 @@ public function toICalendarEvent(string|CarbonInterface $date): ?ICalendarEvent
public function toICalendarEvents(): array
{
return collect($this->days)
->map(fn (Day $day, int $index) => $day->toICalendarEvent($this->event->title, $index))
->map(fn (Day $day, int $index) => $this->decorate(
$day->toICalendarEvent($this->event->title, $index)
))
->all();
}

Expand Down
32 changes: 9 additions & 23 deletions src/Types/RecurringEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,15 @@ public function interval(): int
*/
public function toICalendarEvents(): array
{
$iCalEvent = ICalendarEvent::create($this->event->title)
->uniqueIdentifier($this->event->id())
->startsAt($this->start())
->endsAt($this->end())
->rrule($this->spatieRule());

if ($address = $this->icsAddress()) {
$iCalEvent->address($address);
}

if (! is_null($coords = $this->event->coordinates)) {
$iCalEvent->coordinates($coords['latitude'], $coords['longitude']);
}

if (! is_null($description = $this->event->description)) {
$iCalEvent->description($description);
}

if (! is_null($link = $this->eventUrl())) {
$iCalEvent->url($link);
}

return [$iCalEvent];
return [
$this->decorate(
ICalendarEvent::create($this->event->title)
->uniqueIdentifier($this->event->id())
->startsAt($this->start())
->endsAt($this->end())
->rrule($this->spatieRule())
),
];
}

protected function rule(bool $useEnd = false): RRuleInterface
Expand Down
Loading