WordPress Event Components Implementation

1. Overview

This document describes the technical implementation of the event-info-plugin, which provides the custom post type, shortcodes, and templates for displaying event information on WordPress sites.

2. Plugin Structure

event-info-plugin/
├── event-info-plugin.php         # Plugin bootstrap
├── includes/
│   ├── post-types/
│   │   └── class-event-post-type.php
│   ├── taxonomies/
│   │   └── class-event-taxonomies.php
│   ├── shortcodes/
│   │   ├── class-courses.php
│   │   ├── class-categories.php
│   │   ├── class-program.php
│   │   ├── class-races.php
│   │   ├── class-reg-button.php
│   │   └── class-featured-events.php
│   ├── metaboxes/
│   │   └── class-event-metabox.php
│   └── api/
│       └── class-api-client.php
├── templates/
│   ├── single-epa_event.php
│   └── archive-epa_event.php
└── assets/
    ├── css/
    │   └── featured-events.css
    └── js/
        └── splide.min.js

3. Custom Post Type Registration

class Event_Post_Type {
    public function register() {
        register_post_type('epa_event', [
            'labels' => [
                'name'          => 'Events',
                'singular_name' => 'Event',
                'add_new'       => 'Add Event',
                'add_new_item'  => 'Add New Event',
                'edit_item'     => 'Edit Event',
            ],
            'public'        => true,
            'has_archive'   => true,
            'menu_icon'     => 'dashicons-calendar-alt',
            'supports'      => ['title', 'editor', 'thumbnail', 'excerpt'],
            'rewrite'       => ['slug' => 'event'],
            'show_in_rest'  => true,  // Gutenberg support
        ]);
    }
}

4. Taxonomy Registration

class Event_Taxonomies {
    public function register() {
        // Event Category
        register_taxonomy('event_category', 'epa_event', [
            'labels' => [
                'name'          => 'Event Categories',
                'singular_name' => 'Event Category',
            ],
            'hierarchical' => true,
            'show_in_rest' => true,
            'rewrite'      => ['slug' => 'event-category'],
        ]);

        // Event Venue
        register_taxonomy('event_venue', 'epa_event', [
            'labels' => [
                'name'          => 'Venues',
                'singular_name' => 'Venue',
            ],
            'hierarchical' => false,
            'show_in_rest' => true,
            'rewrite'      => ['slug' => 'venue'],
        ]);
    }
}

5. Shortcode Implementation Pattern

All shortcodes follow a consistent implementation pattern:

5.1. Registration

class Courses_Shortcode {
    public function __construct() {
        add_shortcode('Courses', [$this, 'render']);
        add_action('wp_ajax_epa_get_courses', [$this, 'ajaxHandler']);
        add_action('wp_ajax_nopriv_epa_get_courses', [$this, 'ajaxHandler']);
    }

    public function render($atts) {
        wp_enqueue_script('jquery-ui-tabs');
        wp_enqueue_style('datatables-css');

        return '<div id="epa-courses-container" class="epa-loading">
            <div class="epa-spinner"></div>
        </div>';
    }
}

5.2. AJAX Handler

public function ajaxHandler() {
    check_ajax_referer('epa_nonce', 'nonce');

    $postId = intval($_POST['post_id']);
    $eventId = get_post_meta($postId, '_event_id', true);

    if (!$eventId) {
        wp_send_json_error('No event linked to this page');
    }

    $apiClient = new API_Client();
    $courses = $apiClient->getCourses($eventId);

    if (is_wp_error($courses)) {
        wp_send_json_error($courses->get_error_message());
    }

    wp_send_json_success($this->formatCoursesHtml($courses));
}

5.3. JavaScript Initialisation

jQuery(document).ready(function($) {
    const container = $('#epa-courses-container');
    if (container.length === 0) return;

    $.ajax({
        url: epa_ajax.url,
        type: 'POST',
        data: {
            action: 'epa_get_courses',
            nonce: epa_ajax.nonce,
            post_id: epa_ajax.post_id
        },
        success: function(response) {
            if (response.success) {
                container.html(response.data);
                container.removeClass('epa-loading');
                // Initialize jQuery UI tabs
                container.find('.epa-tabs').tabs();
            } else {
                container.html('<p class="epa-error">' + response.data + '</p>');
            }
        }
    });
});

6. Individual Shortcode Details

6.1. [Courses] - RideWithGPS Integration

Displays race courses via embedded iframe from RideWithGPS.

API Endpoint: GET /api/courses?eventId.equals={eventId}

Output:

<div class="epa-tabs">
    <ul>
        <li><a href="#course-1">50km Route</a></li>
        <li><a href="#course-2">100km Route</a></li>
    </ul>
    <div id="course-1">
        <iframe src="https://ridewithgps.com/embeds?type=route&id=12345"
                width="100%" height="500" frameborder="0"></iframe>
    </div>
    <!-- ... -->
</div>

6.2. [EventCategories] - Age Categories

Displays age categories with birth year calculations.

API Endpoint: GET /api/event-categories?eventId.equals={eventId}&size=100

Calculation Logic:

$currentYear = date('Y');
$birthYearMin = $currentYear - $category->maxAge;
$birthYearMax = $currentYear - $category->minAge;

$birthYearRange = "$birthYearMin - $birthYearMax";

Output: DataTable with columns: Age Range, Year of Birth, Category Name

6.3. [Program] - Event Schedule

Displays the event program/timetable.

API Endpoint: GET /api/program-entries?eventId.equals={eventId}&size=100

Sorting:

usort($entries, function($a, $b) {
    return strtotime($a->dateTime) - strtotime($b->dateTime);
});

Output: DataTable with columns: Event Name, Time (24-hour format)

6.4. [Races] - Race Listing

Displays all races in an event.

API Endpoint: GET /api/races?eventId.equals={eventId}&size=100

Output: DataTable with columns: Name, Start Date/Time, Course Name

6.5. [Registration] - Registration Button

Renders a button linking to the registration portal.

Logic:

$registrationEnd = get_post_meta($postId, '_registration_end', true);
$eventId = get_post_meta($postId, '_event_id', true);

if (strtotime($registrationEnd) < time()) {
    return '<p class="epa-closed">Online registration is now closed</p>';
}

$url = sprintf(
    'https://registration.myriadevents.co.za/register?orgId=%s&eventId=%s',
    $orgId, $eventId
);

return sprintf(
    '<a href="%s" class="epa-reg-button">%s</a>',
    esc_url($url),
    esc_html($buttonText)
);

7.1. Grid Layout

class Featured_Events_Shortcode {
    public function render($atts) {
        $atts = shortcode_atts([
            'count'    => 4,
            'layout'   => 'grid',
            'category' => '',
            'columns'  => 4,
        ], $atts);

        $query = new WP_Query([
            'post_type'      => 'epa_event',
            'posts_per_page' => $atts['count'],
            'meta_key'       => '_event_start_date',
            'orderby'        => 'meta_value',
            'order'          => 'ASC',
            'meta_query'     => [
                [
                    'key'     => '_event_start_date',
                    'value'   => date('Y-m-d'),
                    'compare' => '>=',
                    'type'    => 'DATE'
                ]
            ],
        ]);

        // Apply category filter if specified
        if (!empty($atts['category'])) {
            $query->set('tax_query', [[
                'taxonomy' => 'event_category',
                'field'    => 'slug',
                'terms'    => $atts['category'],
            ]]);
        }

        return $this->renderGrid($query, $atts);
    }
}

7.2. Slider Variant (Splide.js)

private function renderSlider($query, $atts) {
    wp_enqueue_style('splide-css',
        plugins_url('assets/css/splide.min.css', __FILE__));
    wp_enqueue_script('splide-js',
        plugins_url('assets/js/splide.min.js', __FILE__),
        [], '4.1.4', true);

    ob_start();
    ?>
    <div id="featured-events-slider" class="splide">
        <div class="splide__track">
            <ul class="splide__list">
                <?php while ($query->have_posts()): $query->the_post(); ?>
                <li class="splide__slide">
                    <?php $this->renderCard(get_the_ID()); ?>
                </li>
                <?php endwhile; wp_reset_postdata(); ?>
            </ul>
        </div>
    </div>
    <script>
    document.addEventListener('DOMContentLoaded', function() {
        new Splide('#featured-events-slider', {
            type: 'loop',
            perPage: <?php echo $atts['columns']; ?>,
            gap: '1rem',
            autoplay: true,
            interval: 5000,
            pauseOnHover: true,
            breakpoints: {
                1024: { perPage: 3 },
                768:  { perPage: 2 },
                480:  { perPage: 1 }
            }
        }).mount();
    });
    </script>
    <?php
    return ob_get_clean();
}

7.3. Event Card Template

private function renderCard($postId) {
    $startDate = get_post_meta($postId, '_event_start_date', true);
    $venue = get_post_meta($postId, '_venue_name', true);
    $city = get_post_meta($postId, '_venue_city', true);
    $categories = get_the_terms($postId, 'event_category');
    ?>
    <article class="event-card">
        <div class="event-card__image">
            <?php if (has_post_thumbnail()): ?>
                <?php the_post_thumbnail('medium_large'); ?>
            <?php else: ?>
                <div class="event-card__placeholder"></div>
            <?php endif; ?>

            <?php if ($categories): ?>
                <span class="event-card__badge event-card__badge--<?php
                    echo esc_attr($categories[0]->slug); ?>">
                    <?php echo esc_html($categories[0]->name); ?>
                </span>
            <?php endif; ?>
        </div>
        <div class="event-card__content">
            <h3 class="event-card__title"><?php the_title(); ?></h3>
            <div class="event-card__meta">
                <span class="event-card__date">
                    <?php echo date_i18n('D, j M Y', strtotime($startDate)); ?>
                </span>
                <span class="event-card__location">
                    <?php echo esc_html("$venue, $city"); ?>
                </span>
            </div>
            <a href="<?php the_permalink(); ?>" class="event-card__cta">
                Explore
            </a>
        </div>
    </article>
    <?php
}

8. DataTables Integration

All table-based shortcodes use DataTables for enhanced functionality:

$('.epa-datatable').DataTable({
    paging: rowCount > 20,
    searching: false,
    ordering: false,
    info: false,
    responsive: true,
    language: {
        emptyTable: 'No data available'
    }
});

9. Caching Strategy

9.1. Transient Caching

public function getCourses($eventId) {
    $cacheKey = "epa_courses_{$eventId}";
    $cached = get_transient($cacheKey);

    if ($cached !== false) {
        return $cached;
    }

    $response = $this->apiClient->get("/api/courses", [
        'eventId.equals' => $eventId
    ]);

    if (!is_wp_error($response)) {
        set_transient($cacheKey, $response, DAY_IN_SECONDS);
    }

    return $response;
}

9.2. Cache Invalidation

Cache is cleared when:

  • Event post is saved/updated

  • Manual cache clear via admin action

  • Transient expires (24 hours default)

10. Responsive Design

10.1. CSS Grid Breakpoints

.events-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1.5rem;
}

@media (max-width: 1024px) {
    .events-grid { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 768px) {
    .events-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 480px) {
    .events-grid { grid-template-columns: 1fr; }
}