-
Notifications
You must be signed in to change notification settings - Fork 11
Description
Would be nice if an ICS file included the absolute URL of the event, and a description, i.e. the "content" CMS field of a given event collection item. I side stepped the download_link tag with a custom route and Claude 3.7 helped me out on a controller for our specific use case. it's been deployed, working well, and clients are pleased:
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
use Statamic\Facades\Entry;
class EventIcsController extends Controller
{
public function download($id)
{
// Get the event entry
$event = Entry::find($id);
if (!$event || $event->collection()->handle() !== 'events') {
abort(404);
}
// Get event details
$title = $event->get('title');
$location = $event->get('location') ?? '';
$url = $event->absoluteUrl();
// Get description from Bard field and convert to plain text
$description = '';
if ($event->get('content')) {
$description = $this->bardToPlainText($event->get('content'));
}
// THIS IS YOUR EXACT CODE - Preserved exactly as you provided it
if ($event->get('multi_day')) {
$days = $event->get('days');
$first = reset($days);
$last = end($days);
$startDate = $first['date'];
$endDate = $last['date'];
$isAllDay = true; // Multi-day events are all-day events
} else {
$startDate = $event->get('start_date');
$endDate = $event->get('end_date') ?? $startDate;
// Check if this is an all-day event or has specific times
$isAllDay = !($event->get('start_time') && $event->get('end_time'));
// If it has times, append them to the dates
if (!$isAllDay) {
$startTime = $event->get('start_time');
$endTime = $event->get('end_time');
if ($startTime) {
$startDate .= ' ' . $startTime;
}
if ($endTime) {
$endDate .= ' ' . $endTime;
}
}
}
// Get timezone from the event if available, otherwise use the app's default timezone
$timezone = $event->get('timezone') ?? config('app.timezone', 'America/New_York');
$startDateObj = new \DateTime($startDate, new \DateTimeZone($timezone));
$endDateObj = new \DateTime($endDate, new \DateTimeZone($timezone));
// For multi-day events, increment the end date by 1 day (ICS end date is exclusive)
if ($event->get('multi_day')) {
$endDateObj->modify('+1 day');
}
// Current timestamp for DTSTAMP
$nowObj = new \DateTime('now', new \DateTimeZone($timezone));
$now = $nowObj->format('Ymd\THis');
// Create the ICS content
$icsContent = "BEGIN:VCALENDAR\r\n";
$icsContent .= "VERSION:2.0\r\n";
$icsContent .= "PRODID:-//Your Organization//Statamic Events//EN\r\n";
$icsContent .= "CALSCALE:GREGORIAN\r\n";
$icsContent .= "METHOD:PUBLISH\r\n";
$icsContent .= "BEGIN:VEVENT\r\n";
$icsContent .= "UID:" . md5($id) . "@" . $_SERVER['HTTP_HOST'] . "\r\n";
$icsContent .= "DTSTAMP:" . $now . "\r\n";
// Different formatting for all-day vs. timed events
if ($isAllDay) {
// All-day event (just date, no time)
$icsContent .= "DTSTART;VALUE=DATE:" . $startDateObj->format('Ymd') . "\r\n";
$icsContent .= "DTEND;VALUE=DATE:" . $endDateObj->format('Ymd') . "\r\n";
} else {
// Timed event (includes time with timezone)
$icsContent .= "DTSTART;TZID=" . $timezone . ":" . $startDateObj->format('Ymd\THis') . "\r\n";
$icsContent .= "DTEND;TZID=" . $timezone . ":" . $endDateObj->format('Ymd\THis') . "\r\n";
// Add timezone information
$icsContent .= "TZID:" . $timezone . "\r\n";
}
$icsContent .= "SUMMARY:" . $this->escapeString($title) . "\r\n";
if (!empty($location)) {
$icsContent .= "LOCATION:" . $this->escapeString($location) . "\r\n";
}
if (!empty($description)) {
$icsContent .= "DESCRIPTION:" . $this->escapeString($description) . "\r\n";
}
// Add URL to the event page
$icsContent .= "URL:" . $this->escapeString($url) . "\r\n";
$icsContent .= "END:VEVENT\r\n";
$icsContent .= "END:VCALENDAR\r\n";
// Create response with appropriate headers
$filename = Str::slug($title) . '-event.ics';
$response = new Response($icsContent);
$response->header('Content-Type', 'text/calendar; charset=utf-8');
$response->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
return $response;
}
/**
* Escape special characters in string for ICS format
*/
private function escapeString($string)
{
$string = str_replace(array("\\", ";", ","), array("\\\\", "\\;", "\\,"), $string);
$string = preg_replace('/\r\n|\n|\r/', "\\n", $string);
return $string;
}
/**
* Convert Bard field content to plain text
*/
private function bardToPlainText($bardData)
{
if (!is_array($bardData)) {
return '';
}
$plainText = '';
foreach ($bardData as $block) {
if (isset($block['type']) && $block['type'] === 'paragraph' && isset($block['content'])) {
foreach ($block['content'] as $content) {
if (isset($content['text'])) {
$plainText .= $content['text'] . ' ';
}
}
$plainText .= "\n\n";
} elseif (isset($block['type']) && $block['type'] === 'heading' && isset($block['content'])) {
foreach ($block['content'] as $content) {
if (isset($content['text'])) {
$plainText .= $content['text'] . "\n";
}
}
$plainText .= "\n";
} elseif (isset($block['type']) && $block['type'] === 'bullet_list' && isset($block['content'])) {
foreach ($block['content'] as $listItem) {
if (isset($listItem['content'])) {
foreach ($listItem['content'] as $paragraph) {
if (isset($paragraph['content'])) {
foreach ($paragraph['content'] as $content) {
if (isset($content['text'])) {
$plainText .= "• " . $content['text'] . "\n";
}
}
}
}
}
}
$plainText .= "\n";
}
// Add more block types as needed
}
return trim($plainText);
}
}adnankussair