How to Add “Add to Calendar” Buttons to Events in Brilliant Directories (Google, Apple, Outlook, Yahoo)

Introduction
Hi there!
In this tutorial, we’re going to add a nice touch to your events buttons that let visitors add the event to their calendar (Google, Apple, Outlook, and Yahoo). 🙌
This functionality covers 99.9% of use cases. I tested pretty much every scenario: single-day events, multi-day events, recurring events, and of course events with start and end times.
I also included some validations:
- For Yahoo and Outlook, since they don’t allow creating recurring events via link, a nice modal will appear to inform users.
- On mobile, if someone tries to add an event to their Apple Calendar but they’re not using Safari, we’ll let them know they need to open Safari and paste the URL we automatically copied for them. Smooth!
This was definitely a challenge to build, with all the variations and quirks from different calendar platforms… but I’m really happy with the final result. Hope you enjoy it!
How it works
Adding the Widget That Rules Them All
We only need one widget that includes all the logic and layout to make this work.
It uses the default BD variables for events, and also takes into consideration the styling for the buttons.
Let’s go ahead and add the widget!
- Visit managemydirectory and go to Toolbox > Widget Manager in the left sidebar.
- Click the New Widget button.
- Name it event-calendar-integration.
- Copy and paste the code below to add all the functionality and design.
<?php
/**
* Professional Add to Calendar Integration
*
* This script provides a comprehensive "Add to Calendar" functionality that supports:
* - Google Calendar (with recurring events support)
* - Apple Calendar (via .ics file download)
* - Outlook.com Calendar
* - Yahoo Calendar
*
* Features:
* - Handles both single and recurring events
* - iOS Safari compatibility with clipboard fallback
* - Professional modal interface with SVG icons
* - Responsive design for mobile devices
* - PHP 5 compatibility for eval() environments
* - Smart time handling (auto-fills missing times)
* - Cross-platform timezone handling
* - Proper iCalendar (.ics) formatting
*
* @author Alex Cruz - https://www.alexcruz.io
* @version 1.0
* @license MIT
*
* USAGE:
* 1. Ensure $post array contains: post_title, post_start_date, post_expire_date, start_time, end_time, post_content, post_location, post_venue, recurring_type
* 2. Include this file in your PHP page
* 3. The button and modals will be automatically rendered
*
* REQUIRED DEPENDENCIES:
* - Bootstrap CSS/JS (for modals)
* - Font Awesome 4 (for calendar icon)
* - jQuery (for modal functionality)
*/
$eventTitleRaw = $post['post_title'];
$eventTitle = urlencode($eventTitleRaw);
// Create working copies of the time variables to avoid modifying original $post
$workingStartTime = $post['start_time'];
$workingEndTime = $post['end_time'];
// Check if times are provided and valid
$hasValidStartTime = !empty($workingStartTime) && $workingStartTime !== 'N/A' && $workingStartTime !== 'n/a';
$hasValidEndTime = !empty($workingEndTime) && $workingEndTime !== 'N/A' && $workingEndTime !== 'n/a';
// Smart time handling logic
if ($hasValidStartTime && $hasValidEndTime) {
// Both times provided - use as is
$hasValidTimes = true;
} elseif ($hasValidStartTime && !$hasValidEndTime) {
// Only start time provided - add 1 hour for end time
$startTimeObj = DateTime::createFromFormat('g:i A', $workingStartTime);
if ($startTimeObj) {
$endTimeObj = clone $startTimeObj;
$endTimeObj->modify('+1 hour');
$workingEndTime = $endTimeObj->format('g:i A');
$hasValidTimes = true;
} else {
$hasValidTimes = false;
}
} elseif (!$hasValidStartTime && $hasValidEndTime) {
// Only end time provided - subtract 1 hour for start time
$endTimeObj = DateTime::createFromFormat('g:i A', $workingEndTime);
if ($endTimeObj) {
$startTimeObj = clone $endTimeObj;
$startTimeObj->modify('-1 hour');
$workingStartTime = $startTimeObj->format('g:i A');
$hasValidTimes = true;
} else {
$hasValidTimes = false;
}
} else {
// No valid times provided - all day event
$hasValidTimes = false;
}
// Determine if the event is recurring to correctly set the end date of the first occurrence
// and the end date of the entire series.
$isRecurring = isset($post['recurring_type']) && (int)$post['recurring_type'] > 0;
// Only process dates if both times are valid
if ($hasValidTimes) {
// Dates need to be in UTC format: YYYYMMDDTHHMMSSZ for the calendar links.
$startDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingStartTime);
if ($isRecurring) {
// For recurring events, the end date of the *first* occurrence is on the same day.
// The post_expire_date is when the recurrence series ends.
$endDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingEndTime);
} else {
// For single events, post_expire_date is the end date of that single event.
$endDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime);
}
} else {
// If times are not valid, set objects to null
$startDateObj = null;
$endDateObj = null;
}
// Check if DateTime objects were created successfully
if ($hasValidTimes) {
if (!$startDateObj) {
// Fallback: create with event date if parsing fails
$startDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingStartTime);
}
if (!$endDateObj) {
// Fallback: create with event date if parsing fails
if ($isRecurring) {
$endDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingEndTime);
} else {
$endDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime);
}
}
}
// We get the UNIX timestamp from the DateTime objects.
if ($hasValidTimes) {
$eventStartDate_timestamp = $startDateObj ? $startDateObj->getTimestamp() : time();
$eventEndDate_timestamp = $endDateObj ? $endDateObj->getTimestamp() : time();
} else {
// If no valid times, create all-day event using the event dates
$startDateOnly = DateTime::createFromFormat('Ymd', $post['post_start_date']);
$endDateOnly = DateTime::createFromFormat('Ymd', $post['post_expire_date']);
$eventStartDate_timestamp = $startDateOnly ? $startDateOnly->getTimestamp() : time();
$eventEndDate_timestamp = $endDateOnly ? $endDateOnly->getTimestamp() : time();
}
// Manually construct date strings to be eval-safe, avoiding special characters in the format string.
if ($hasValidTimes) {
$eventStartDate = gmdate('Ymd', $eventStartDate_timestamp) . 'T' . gmdate('His', $eventStartDate_timestamp) . 'Z';
$eventEndDate = gmdate('Ymd', $eventEndDate_timestamp) . 'T' . gmdate('His', $eventEndDate_timestamp) . 'Z';
} else {
// If no valid times, create all-day event format (just the date, no time)
$eventStartDate = gmdate('Ymd', $eventStartDate_timestamp);
$eventEndDate = gmdate('Ymd', $eventEndDate_timestamp);
}
// We use the post content for the description. We strip HTML tags to keep it clean.
$eventDescriptionRaw = strip_tags($post['post_content']);
// Add venue information if available - put venue first for better visibility
if (!empty($post['post_venue'])) {
if (!empty($eventDescriptionRaw)) {
$eventDescriptionRaw = "Venue: " . $post['post_venue'] . " - " . $eventDescriptionRaw;
} else {
$eventDescriptionRaw = "Venue: " . $post['post_venue'];
}
}
$eventDescription = urlencode($eventDescriptionRaw);
// Location data from the event post
// Use post_location if available
$eventLocationRaw = !empty($post['post_location']) ? $post['post_location'] : '';
$eventLocation = urlencode($eventLocationRaw);
// --- Generate Calendar URLs ---
// Google Calendar
$googleCalendarUrl = "https://calendar.google.com/calendar/render?action=TEMPLATE&text={$eventTitle}&dates={$eventStartDate}/{$eventEndDate}&details={$eventDescription}&location={$eventLocation}";
// Handle recurring events for Google Calendar
if ($isRecurring) {
$recurringType = (int)$post['recurring_type'];
$rrule = '';
if ($recurringType === 1) {
$rrule = 'FREQ=DAILY';
} elseif ($recurringType % 7 === 0) {
$interval = $recurringType / 7;
$rrule = 'FREQ=WEEKLY;INTERVAL=' . $interval;
}
if ($rrule !== '') {
// Add the end date for the recurrence series.
// For Google Calendar, use the exact expire date without adding a day
$recurrenceUntilDateObj = DateTime::createFromFormat('Ymd', $post['post_expire_date']);
if ($recurrenceUntilDateObj) {
$untilDate = $recurrenceUntilDateObj->format('Ymd') . 'T000000Z';
$rrule .= ';UNTIL=' . $untilDate;
}
$googleCalendarUrl .= '&recur=RRULE:' . $rrule;
}
}
// Outlook Calendar
// Outlook requires a specific ISO 8601 format with separators.
// We build it manually to be eval-safe.
$outlookStartDate = gmdate('Y-m-d', $eventStartDate_timestamp) . 'T' . gmdate('H:i:s', $eventStartDate_timestamp) . 'Z';
// For recurring events, use the expire date as the end date to show the full range
if ($isRecurring) {
$outlookEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $post['end_time']);
$outlookEndDate_timestamp = $outlookEndDateObj ? $outlookEndDateObj->getTimestamp() : $eventEndDate_timestamp;
$outlookEndDate = gmdate('Y-m-d', $outlookEndDate_timestamp) . 'T' . gmdate('H:i:s', $outlookEndDate_timestamp) . 'Z';
} else {
$outlookEndDate = gmdate('Y-m-d', $eventEndDate_timestamp) . 'T' . gmdate('H:i:s', $eventEndDate_timestamp) . 'Z';
}
$outlookCalendarUrl = "https://outlook.live.com/calendar/0/deeplink/compose?path=/calendar/action/compose&rru=addevent&subject={$eventTitle}&startdt={$outlookStartDate}&enddt={$outlookEndDate}&body={$eventDescription}&location={$eventLocation}";
// Yahoo Calendar
// Yahoo requires the time to be treated as UTC from the start, without conversion from the server's timezone.
// We create a new DateTime object with the UTC timezone explicitly set for this.
$utc = new DateTimeZone('UTC');
if ($hasValidTimes) {
$yahooStartDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingStartTime, $utc);
// For recurring events, use the expire date as the end date to show the full range
if ($isRecurring) {
$yahooEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime, $utc);
} else {
$yahooEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime, $utc);
}
} else {
$yahooStartDateObj = null;
$yahooEndDateObj = null;
}
// Check if DateTime objects were created successfully for Yahoo
if ($hasValidTimes) {
if (!$yahooStartDateObj) {
// Fallback: create with event date if parsing fails
$yahooStartDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingStartTime, $utc);
}
if (!$yahooEndDateObj) {
// Fallback: create with event date if parsing fails
if ($isRecurring) {
$yahooEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime, $utc);
} else {
$yahooEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime, $utc);
}
}
}
// Manually construct the date string to be eval-safe.
if ($hasValidTimes) {
$yahooStartDate = $yahooStartDateObj ? $yahooStartDateObj->format('Ymd') . 'T' . $yahooStartDateObj->format('His') : '';
$yahooEndDate = $yahooEndDateObj ? $yahooEndDateObj->format('Ymd') . 'T' . $yahooEndDateObj->format('His') : '';
} else {
// For Yahoo, when no times provided, use current browser time
// This will be handled by JavaScript to get the actual browser time
$startDateOnly = DateTime::createFromFormat('Ymd', $post['post_start_date']);
$endDateOnly = DateTime::createFromFormat('Ymd', $post['post_expire_date']);
// Use current server time as fallback (will be overridden by JavaScript)
$currentTime = date('His');
$yahooStartDate = $startDateOnly ? $startDateOnly->format('Ymd') . 'T' . $currentTime : '';
$yahooEndDate = $endDateOnly ? $endDateOnly->format('Ymd') . 'T' . $currentTime : '';
}
$yahooCalendarUrl = "https://calendar.yahoo.com/?v=60&view=d&type=20&title={$eventTitle}&st={$yahooStartDate}&et={$yahooEndDate}&desc={$eventDescription}&in_loc={$eventLocation}";
// iCalendar (.ics) file for Apple Calendar
// For Apple Calendar, we need to handle the end date correctly for recurring events
// For recurring events, the end date of the FIRST occurrence should be on the same day as start
// For single events, the end date can be on a different day
if ($isRecurring) {
// For recurring events, end time is on the same day as start
$icsStartDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingStartTime);
$icsEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingEndTime);
} else {
// For single events, end time can be on a different day
$icsStartDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingStartTime);
$icsEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime);
}
// Check if DateTime objects were created successfully for Apple Calendar
if ($hasValidTimes) {
if (!$icsStartDateObj) {
// Fallback: create with event date if parsing fails
$icsStartDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingStartTime);
}
if (!$icsEndDateObj) {
// Fallback: create with event date if parsing fails
if ($isRecurring) {
$icsEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_start_date'] . ' ' . $workingEndTime);
} else {
$icsEndDateObj = DateTime::createFromFormat('Ymd g:i A', $post['post_expire_date'] . ' ' . $workingEndTime);
}
}
}
// Manually construct floating date-time string to be eval-safe.
if ($hasValidTimes) {
$icsStartDate = $icsStartDateObj ? $icsStartDateObj->format('Ymd') . 'T' . $icsStartDateObj->format('His') : '';
$icsEndDate = $icsEndDateObj ? $icsEndDateObj->format('Ymd') . 'T' . $icsEndDateObj->format('His') : '';
} else {
// For all-day events, use just the date without time
$startDateOnly = DateTime::createFromFormat('Ymd', $post['post_start_date']);
$endDateOnly = DateTime::createFromFormat('Ymd', $post['post_expire_date']);
$icsStartDate = $startDateOnly ? $startDateOnly->format('Ymd') : '';
$icsEndDate = $endDateOnly ? $endDateOnly->format('Ymd') : '';
}
// This version is heavily refactored for maximum compatibility with eval() environments.
// It avoids function declarations and complex array structures.
// --- Step 1: Build and fold each content line ---
$crlf = chr(13) . chr(10); // Use chr() for robust newline creation in eval()
$summaryLine = 'SUMMARY:' . $eventTitleRaw;
$summaryLineFolded = wordwrap($summaryLine, 75, $crlf . ' ', true);
$descriptionLine = 'DESCRIPTION:' . $eventDescriptionRaw;
$descriptionLineFolded = wordwrap($descriptionLine, 75, $crlf . ' ', true);
$locationLine = 'LOCATION:' . $eventLocationRaw;
$locationLineFolded = wordwrap($locationLine, 75, $crlf . ' ', true);
// --- Step 2: Assemble the final ICS content via simple concatenation ---
$icsContent = 'BEGIN:VCALENDAR' . $crlf;
$icsContent .= 'VERSION:2.0' . $crlf;
$icsContent .= 'PRODID:-//My Web Directory//NONSGML v1.0//EN' . $crlf;
$icsContent .= 'BEGIN:VEVENT' . $crlf;
$icsContent .= 'UID:' . uniqid() . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'example.com') . $crlf;
// Manually construct DTSTAMP to be eval-safe.
$icsContent .= 'DTSTAMP:' . gmdate('Ymd') . 'T' . gmdate('His') . 'Z' . $crlf;
$icsContent .= 'DTSTART:' . $icsStartDate . $crlf;
$icsContent .= 'DTEND:' . $icsEndDate . $crlf;
// Add RRULE for recurring events in iCalendar format
if ($isRecurring) {
$recurringType = (int)$post['recurring_type'];
$rrule = '';
if ($recurringType === 1) {
$rrule = 'FREQ=DAILY';
} elseif ($recurringType % 7 === 0) {
$interval = $recurringType / 7;
// Get the day of week for the start date (1=Monday, 2=Tuesday, etc.)
$startDateObj = DateTime::createFromFormat('Ymd', $post['post_start_date']);
$dayOfWeek = '';
if ($startDateObj) {
switch ($startDateObj->format('N')) {
case 1: $dayOfWeek = 'MO'; break; // Monday
case 2: $dayOfWeek = 'TU'; break; // Tuesday
case 3: $dayOfWeek = 'WE'; break; // Wednesday
case 4: $dayOfWeek = 'TH'; break; // Thursday
case 5: $dayOfWeek = 'FR'; break; // Friday
case 6: $dayOfWeek = 'SA'; break; // Saturday
case 7: $dayOfWeek = 'SU'; break; // Sunday
}
}
$rrule = 'FREQ=WEEKLY;INTERVAL=' . $interval;
if ($dayOfWeek) {
$rrule .= ';BYDAY=' . $dayOfWeek;
}
}
if ($rrule !== '') {
// For Apple Calendar, the UNTIL date should be the last occurrence date
// We need to calculate the last occurrence based on the pattern
$startDateObj = DateTime::createFromFormat('Ymd', $post['post_start_date']);
$endDateObj = DateTime::createFromFormat('Ymd', $post['post_expire_date']);
if ($startDateObj && $endDateObj) {
// For recurring events, post_expire_date is the date of the LAST occurrence
// But we need to add one day to make it inclusive, like Google Calendar does
$lastOccurrenceDate = DateTime::createFromFormat('Ymd', $post['post_expire_date']);
if ($lastOccurrenceDate) {
// Add one day to make the end date inclusive
$lastOccurrenceDate->modify('+1 day');
$untilDate = $lastOccurrenceDate->format('Ymd') . 'T000000';
$rrule .= ';UNTIL=' . $untilDate;
}
}
$icsContent .= 'RRULE:' . $rrule . $crlf;
}
}
$icsContent .= $summaryLineFolded . $crlf;
$icsContent .= $descriptionLineFolded . $crlf;
$icsContent .= $locationLineFolded . $crlf;
$icsContent .= 'END:VEVENT' . $crlf;
$icsContent .= 'END:VCALENDAR';
// --- Step 3: Create the final URL ---
$icsUrl = 'data:text/calendar;charset=utf8,' . rawurlencode($icsContent);
?>
<style>
/* Custom styles for 'Add to Calendar' functionality */
#addToCalendarBtn {
background-color: #222222;
color: #ffffff;
border: none;
border-radius: 5px;
padding: 12px 25px;
font-size: 16px;
font-weight: bold;
transition: background-color 0.3s ease;
margin-bottom: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#addToCalendarBtn:hover {
background-color: #444444;
}
#addToCalendarModal .modal-content {
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
#addToCalendarModal .modal-header {
border-bottom: none;
padding: 25px 25px 10px;
text-align: center;
}
#addToCalendarModal .modal-title {
font-size: 24px;
font-weight: bold;
width: 100%;
}
#addToCalendarModal .modal-header .close {
display: none;
}
#addToCalendarModal .modal-header .close:hover {
opacity: 1;
}
#addToCalendarModal .modal-body {
padding: 10px 40px 20px;
}
#addToCalendarModal .modal-body p {
text-align: center;
margin-bottom: 25px;
font-size: 16px;
color: #666;
}
#recurringEventsModal .modal-header .close {
display: none;
}
.calendar-options-list {
padding-left: 0;
list-style: none;
margin-bottom: 0;
}
.calendar-options-list li a {
display: flex;
align-items: center;
padding: 15px 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 10px;
text-decoration: none;
color: #333;
font-size: 18px;
font-weight: 500;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.calendar-options-list li a:hover {
background-color: #f5f5f5;
border-color: #ccc;
}
.calendar-options-list li a svg {
width: 25px;
height: 25px;
margin-right: 15px;
flex-shrink: 0;
}
#addToCalendarModal .modal-footer {
border-top: none;
padding: 15px 25px 25px;
text-align: center;
}
#addToCalendarModal .modal-footer .btn-default {
background-color: #333;
color: white;
border: none;
padding: 10px 30px;
border-radius: 5px;
font-size: 16px;
transition: background-color 0.3s;
}
#addToCalendarModal .modal-footer .btn-default:hover {
background-color: #555;
}
/* Styles for iOS Safari modal */
.safari-modal-icon-wrapper {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
background-color: white;
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1060;
border: 1px solid #ddd;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.safari-modal-icon {
width: 44px;
height: 44px;
border-radius: 50%;
background-color: #dc3545;
color: white;
font-size: 28px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
#iosSafariModal .modal-content {
padding-top: 30px;
text-align: center;
overflow: visible;
}
#iosSafariModal .modal-header {
border-bottom: none;
text-align: center;
}
#iosSafariModal .modal-title {
font-weight: bold;
font-size: 22px;
width: 100%;
}
#iosSafariModal .modal-body p {
font-size: 16px;
color: #333;
margin-bottom: 15px;
}
#iosSafariModal .modal-body ol {
text-align: left;
display: inline-block;
padding-left: 25px;
margin-top: 10px;
}
#iosSafariModal .modal-body ol li {
margin-bottom: 10px;
}
#iosSafariModal .modal-footer {
border-top: none;
padding-top: 0;
}
#iosSafariModal .modal-dialog {
margin-top: 50px;
}
@media (max-width: 767px) {
#addToCalendarBtn {
width: 100%;
}
}
</style>
<!-- Button to trigger modal -->
<button type="button" class="btn" id="addToCalendarBtn" data-toggle="modal" data-target="#addToCalendarModal">
<i class="fa fa-calendar"></i> Add to My Calendar
</button>
<!-- Modal -->
<div class="modal fade" id="addToCalendarModal" tabindex="-1" role="dialog" aria-labelledby="addToCalendarModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="addToCalendarModalLabel">Add Event to Your Calendar</h4>
</div>
<div class="modal-body">
<p>Choose your preferred calendar to save this event.</p>
<ul class="calendar-options-list">
<li>
<a id="appleCalendarLink" href="<?php echo $icsUrl; ?>" download="event.ics">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 245.657"><path d="M167.084 130.514c-.308-31.099 25.364-46.022 26.511-46.761-14.429-21.107-36.91-24.008-44.921-24.335-19.13-1.931-37.323 11.27-47.042 11.27-9.692 0-24.67-10.98-40.532-10.689-20.849.308-40.07 12.126-50.818 30.799-21.661 37.581-5.54 93.281 15.572 123.754 10.313 14.923 22.612 31.688 38.764 31.089 15.549-.612 21.433-10.073 40.242-10.073s24.086 10.073 40.546 9.751c16.737-.308 27.34-15.214 37.585-30.187 11.855-17.318 16.714-34.064 17.009-34.925-.372-.168-32.635-12.525-32.962-49.68l.045-.013zm-30.917-91.287C144.735 28.832 150.524 14.402 148.942 0c-12.344.503-27.313 8.228-36.176 18.609-7.956 9.216-14.906 23.904-13.047 38.011 13.786 1.075 27.862-7.004 36.434-17.376z"></path></svg>
Apple
</a>
</li>
<li>
<a href="<?php echo $googleCalendarUrl; ?>" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><path d="M152.637 47.363H47.363v105.273h105.273z" fill="#fff"></path><path d="M152.637 200L200 152.637h-47.363z" fill="#f72a25"></path><path d="M200 47.363h-47.363v105.273H200z" fill="#fbbc04"></path><path d="M152.637 152.637H47.363V200h105.273z" fill="#34a853"></path><path d="M0 152.637v31.576A15.788 15.788 0 0 0 15.788 200h31.576v-47.363z" fill="#188038"></path><path d="M200 47.363V15.788A15.79 15.79 0 0 0 184.212 0h-31.575v47.363z" fill="#1967d2"></path><path d="M15.788 0A15.79 15.79 0 0 0 0 15.788v136.849h47.363V47.363h105.274V0z" fill="#4285f4"></path><path d="M68.962 129.02c-3.939-2.653-6.657-6.543-8.138-11.67l9.131-3.76c.83 3.158 2.279 5.599 4.346 7.341 2.051 1.742 4.557 2.588 7.471 2.588 2.995 0 5.55-.911 7.699-2.718 2.148-1.823 3.223-4.134 3.223-6.934 0-2.865-1.139-5.208-3.402-7.031s-5.111-2.718-8.496-2.718h-5.273v-9.033h4.736c2.913 0 5.387-.781 7.389-2.376 2.002-1.579 2.995-3.743 2.995-6.494 0-2.441-.895-4.395-2.686-5.859s-4.053-2.197-6.803-2.197c-2.686 0-4.818.716-6.396 2.148s-2.767 3.255-3.451 5.273l-9.033-3.76c1.204-3.402 3.402-6.396 6.624-8.984s7.34-3.89 12.337-3.89c3.695 0 7.031.716 9.977 2.148s5.257 3.418 6.934 5.941c1.676 2.539 2.507 5.387 2.507 8.545 0 3.223-.781 5.941-2.327 8.187-1.546 2.23-3.467 3.955-5.729 5.143v.537a17.39 17.39 0 0 1 7.34 5.729c1.904 2.572 2.865 5.632 2.865 9.212s-.911 6.771-2.718 9.57c-1.823 2.799-4.329 5.013-7.52 6.624s-6.787 2.425-10.775 2.425c-4.622 0-8.887-1.318-12.826-3.988zm56.087-45.312l-10.026 7.243-5.013-7.601 17.985-12.972h6.901v61.198h-9.847z" fill="#1a73e8"></path></svg>
Google
</a>
</li>
<li>
<a href="<?php echo $outlookCalendarUrl; ?>" target="_blank" <?php if ($isRecurring) { echo 'class="recurring-event-link" data-calendar-url="' . $outlookCalendarUrl . '"'; } ?>>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 175"><path d="M178.725 0H71.275A8.775 8.775 0 0 0 62.5 8.775v9.975l60.563 18.75L187.5 18.75V8.775A8.775 8.775 0 0 0 178.725 0z" fill="#0364b8"></path><path d="M197.813 96.281c.915-2.878 2.187-5.855 2.187-8.781-.002-1.485-.795-2.857-1.491-3.26l-68.434-38.99a9.37 9.37 0 0 0-9.244-.519c-.312.154-.614.325-.906.512l-67.737 38.6-.025.013-.075.044a4.16 4.16 0 0 0-2.088 3.6c.541 2.971 1.272 5.904 2.188 8.781l71.825 52.532z" fill="#0a2767"></path><path d="M150 18.75h-43.75L93.619 37.5l12.631 18.75L150 93.75h37.5v-37.5z" fill="#28a8ea"></path><path d="M150 18.75h37.5v37.5H150z" fill="#50d9ff"></path><path d="M150 93.75l-43.75-37.5H62.5v37.5l43.75 37.5 67.7 11.05z" fill="#0364b8"></path><path d="M106.25 56.25v37.5H150v-37.5zM150 93.75v37.5h37.5v-37.5zm-87.5-75h43.75v37.5H62.5z" fill="#0078d4"></path><path d="M62.5 93.75h43.75v37.5H62.5z" fill="#064a8c"></path><path d="M126.188 145.113l-73.706-53.75 3.094-5.438 68.181 38.825a3.3 3.3 0 0 0 2.625-.075l68.331-38.937 3.1 5.431z" fill="#0a2767" opacity=".5"></path><path d="M197.919 91.106l-.088.05-.019.013-67.738 38.588c-2.736 1.764-6.192 1.979-9.125.569l23.588 31.631 51.588 11.257v-.001c2.434-1.761 3.876-4.583 3.875-7.587V87.5c.001 1.488-.793 2.862-2.081 3.606z" fill="#1490df"></path><path d="M200 165.625v-4.613l-62.394-35.55-7.531 4.294a9.356 9.356 0 0 1-9.125.569l23.588 31.631 51.588 11.231v.025a9.362 9.362 0 0 0 3.875-7.588z" opacity=".05"></path><path d="M199.688 168.019l-68.394-38.956-1.219.688c-2.734 1.766-6.19 1.984-9.125.575l23.588 31.631 51.587 11.256v.001a9.38 9.38 0 0 0 3.562-5.187z" opacity=".1"></path><path d="M51.455 90.721c-.733-.467-1.468-1.795-1.455-3.221v78.125c-.007 5.181 4.194 9.382 9.375 9.375h131.25c1.395-.015 2.614-.366 3.813-.813.638-.258 1.252-.652 1.687-.974z" fill="#28a8ea"></path><path d="M112.5 141.669V39.581a8.356 8.356 0 0 0-8.331-8.331H62.687v46.6l-10.5 5.987-.031.012-.075.044A4.162 4.162 0 0 0 50 87.5v.031-.031V150h54.169a8.356 8.356 0 0 0 8.331-8.331z" opacity=".1"></path><path d="M106.25 147.919V45.831a8.356 8.356 0 0 0-8.331-8.331H62.687v40.35l-10.5 5.987-.031.012-.075.044A4.162 4.162 0 0 0 50 87.5v.031-.031 68.75h47.919a8.356 8.356 0 0 0 8.331-8.331z" opacity=".2"></path><path d="M106.25 135.419V45.831a8.356 8.356 0 0 0-8.331-8.331H62.687v40.35l-10.5 5.987-.031.012-.075.044A4.162 4.162 0 0 0 50 87.5v.031-.031 56.25h47.919a8.356 8.356 0 0 0 8.331-8.331z" opacity=".2"></path><path d="M100 135.419V45.831a8.356 8.356 0 0 0-8.331-8.331H62.687v40.35l-10.5 5.987-.031.012-.075.044A4.162 4.162 0 0 0 50 87.5v.031-.031 56.25h41.669a8.356 8.356 0 0 0 8.331-8.331z" opacity=".2"></path><path d="M8.331 37.5h83.337A8.331 8.331 0 0 1 100 45.831v83.338a8.331 8.331 0 0 1-8.331 8.331H8.331A8.331 8.331 0 0 1 0 129.169V45.831A8.331 8.331 0 0 1 8.331 37.5z" fill="#0078d4"></path><path d="M24.169 71.675a26.131 26.131 0 0 1 10.263-11.337 31.031 31.031 0 0 1 16.313-4.087 28.856 28.856 0 0 1 15.081 3.875 25.875 25.875 0 0 1 9.988 10.831 34.981 34.981 0 0 1 3.5 15.938 36.881 36.881 0 0 1-3.606 16.662 26.494 26.494 0 0 1-10.281 11.213 30 30 0 0 1-15.656 3.981 29.556 29.556 0 0 1-15.425-3.919 26.275 26.275 0 0 1-10.112-10.85 34.119 34.119 0 0 1-3.544-15.744 37.844 37.844 0 0 1 3.481-16.563zm10.938 26.613a16.975 16.975 0 0 0 5.769 7.463 15.069 15.069 0 0 0 9.019 2.719 15.831 15.831 0 0 0 9.631-2.806 16.269 16.269 0 0 0 5.606-7.481 28.913 28.913 0 0 0 1.787-10.406 31.644 31.644 0 0 0-1.687-10.538 16.681 16.681 0 0 0-5.413-7.75 14.919 14.919 0 0 0-9.544-2.956 15.581 15.581 0 0 0-9.231 2.744 17.131 17.131 0 0 0-5.9 7.519 29.85 29.85 0 0 0-.044 21.5z" fill="#fff"></path></svg>
Outlook.com
</a>
</li>
<li>
<a href="<?php echo $yahooCalendarUrl; ?>" target="_blank" <?php if ($isRecurring) { echo 'class="recurring-event-link" data-calendar-url="' . $yahooCalendarUrl . '"'; } ?> <?php if (!$hasValidTimes) { echo 'id="yahooCalendarLink" data-base-url="' . $yahooCalendarUrl . '"'; } ?>>
<svg fill="#5f01d1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 177.803"><path d="M0 43.284h38.144l22.211 56.822 22.5-56.822h37.135L64.071 177.803H26.694l15.308-35.645L.001 43.284zm163.235 45.403H121.64L158.558 0 200 .002zm-30.699 8.488c12.762 0 23.108 10.346 23.108 23.106s-10.345 23.106-23.108 23.106a23.11 23.11 0 0 1-23.104-23.106 23.11 23.11 0 0 1 23.104-23.106z"></path></svg>
Yahoo
</a>
</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- iOS Safari Modal -->
<div class="modal fade" id="iosSafariModal" tabindex="-1" role="dialog" aria-labelledby="iosSafariModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="safari-modal-icon-wrapper">
<div class="safari-modal-icon">!</div>
</div>
<div class="modal-header">
<h4 class="modal-title" id="iosSafariModalLabel">Open Safari</h4>
</div>
<div class="modal-body">
<p>Unfortunately, iOS has some problems generating and opening the calendar file outside of Safari.</p>
<p>We automatically copied the calendar URL into your clipboard.</p>
<ol>
<li>Open Safari</li>
<li>Paste the clipboard content and go.</li>
</ol>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Recurring Events Modal -->
<div class="modal fade" id="recurringEventsModal" tabindex="-1" role="dialog" aria-labelledby="recurringEventsModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="recurringEventsModalLabel">🔄 Recurring Event</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p>When you click the link, the calendar will open with only the first date of the event.</p>
<p><strong>To set up the recurrence:</strong></p>
<ol>
<li>The new event form will open</li>
<li>Look for the "Repeat" or "Recurrence" option</li>
<li>Configure the frequency (daily, weekly, etc.)</li>
<li>Set the end date to: <strong><?php echo date('F j, Y', strtotime($post['post_expire_date'])); ?></strong></li>
<li>Save the event</li>
</ol>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="continueToCalendar">Continue to Calendar</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
function isRealIOSDevice() {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document);
}
if (isRealIOSDevice()) {
var appleLink = document.getElementById('appleCalendarLink');
if (appleLink) {
appleLink.addEventListener('click', function(event) {
event.preventDefault();
var icsUrl = this.getAttribute('href');
// Modern clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(icsUrl).then(function() {
// Success
$('#addToCalendarModal').modal('hide');
$('#iosSafariModal').modal('show');
}, function(err) {
// Error
console.error('Could not copy text: ', err);
// Fallback or alert
alert('Could not copy the link. Please tap and hold the link to copy it.');
});
} else {
// Fallback for older browsers
var textArea = document.createElement("textarea");
textArea.value = icsUrl;
textArea.style.position="fixed"; //avoid scrolling to bottom
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
$('#addToCalendarModal').modal('hide');
$('#iosSafariModal').modal('show');
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
alert('Could not copy the link. Please tap and hold the link to copy it.');
}
document.body.removeChild(textArea);
}
});
}
}
});
// Handle recurring events for Outlook and Yahoo
document.addEventListener('DOMContentLoaded', function() {
var recurringLinks = document.querySelectorAll('.recurring-event-link');
recurringLinks.forEach(function(link) {
link.addEventListener('click', function(event) {
event.preventDefault();
var calendarUrl = this.getAttribute('data-calendar-url');
// Store the URL for the continue button
document.getElementById('continueToCalendar').setAttribute('data-url', calendarUrl);
// Show the recurring events modal
$('#addToCalendarModal').modal('hide');
$('#recurringEventsModal').modal('show');
});
});
// Handle continue button click
document.getElementById('continueToCalendar').addEventListener('click', function() {
var url = this.getAttribute('data-url');
if (url) {
window.open(url, '_blank');
}
$('#recurringEventsModal').modal('hide');
});
// Handle Yahoo Calendar with current browser time
var yahooLink = document.getElementById('yahooCalendarLink');
if (yahooLink) {
yahooLink.addEventListener('click', function(event) {
event.preventDefault();
var baseUrl = this.getAttribute('data-base-url');
if (baseUrl) {
// Get current browser time
var now = new Date();
var currentHour = now.getHours().toString().padStart(2, '0');
var currentMinute = now.getMinutes().toString().padStart(2, '0');
var currentSecond = now.getSeconds().toString().padStart(2, '0');
var currentTime = currentHour + currentMinute + currentSecond;
// Replace the time in the URL
var updatedUrl = baseUrl.replace(/T\d{6}/g, 'T' + currentTime);
// Open the updated URL
window.open(updatedUrl, '_blank');
}
});
}
});
</script>
• Finally, click Save Changes.
Adding the Widget to the Event Page
This widget goes inside the featured events layout, so let’s add it there:
- Visit ManageMyDirectory and go to My Content > Edit Post Settings in the left sidebar.
- Use the search box to find Events, then click Edit.
- Select the Detail Page Design tab.
- Click the button Click To View/Edit Code.
- Add the following code after the line 6, or wherever you’d like the button to appear.
[widget=event-calendar-integration]
- Here’s how it should look:

- Finally, click Save Changes.
QA
Now that everything’s set up, let’s make sure it works beautifully! 🙌
If you don’t have any events yet, go ahead and create a few test cases using the list below. These examples will help you validate that your calendar buttons behave correctly in all the common scenarios your users might encounter.
Single Events:
- Single-day event – Start and end time on the same day (e.g., 9 AM – 5 PM)
- Multi-day event – Starts Monday, ends Wednesday with specific times
- All-day event – Single day, no specific time
- Event with location – Single-day event with venue details
Recurring Events – Daily:
- Daily for 1 week – Every day for 7 days
- Daily for 1 month – Every day for 30 days
- Weekdays only – Monday to Friday for 2 weeks
Recurring Events – Weekly:
- Weekly for 1 month – Every Monday for 4 weeks
- Weekly for 3 months – Every Tuesday for 12 weeks
- Bi-weekly for 2 months – Every other Friday for 8 weeks
🎯 These 10 test cases cover 99% of what your visitors will throw at your events. If your “Add to Calendar” buttons pass these, you’re good to go!
Take a moment to test them out and celebrate the little wins as everything clicks into place. You’ve just added a powerful feature to your event listings — nicely done! 🎉
Conclusion
Congratulations! 🎉 You’ve now implemented an “Add to Calendar” button, giving your visitors a simple way to make sure they never miss an event.
I hope this was helpful for both you and your members. If you run into any issues, feel free to drop a comment on this tutorial over on my Facebook page — I’ll be adding a comment section here soon too.
I really hope you enjoyed this tutorial as much as I enjoyed creating it. I put a lot of heart into it, and if you found it useful, I’d love it if you’d consider signing up! 🙌