wpcoding.ru wordpress WP Coding

Как создать адаптивный календарь событий в WordPress с фильтрами и ajax

Календарь событий — очень востребованный элемент для многих сайтов на WordPress, особенно если нужно показывать мероприятия, записи блога по датам или бронирования. В этой статье я подробно расскажу, как создать собственный адаптивный календарь событий с возможностью фильтрации и загрузкой данных через AJAX. Такой подход позволит избежать перезагрузки страницы и улучшит удобство пользователей.

Выбор подхода для реализации календаря событий в WordPress

Существует множество готовых плагинов для календаря событий, например, The Events Calendar, Event Organiser или Modern Events Calendar. Однако если у вас специфические требования или хочется легковесное, кастомное решение без лишнего кода, лучше написать календарь самостоятельно.

Основные задачи, которые мы решим:

  • Вывод календаря на странице с выделением дней, в которые есть события
  • Фильтрация событий по категориям или типам через AJAX
  • Адаптивная верстка для корректного отображения на мобильных устройствах
  • Подключение данных из пользовательского типа записи (custom post type)

Для примера создадим кастомный тип записи wpcoding_event и таксономию event_category.

Регистрация кастомного типа записи и таксономии

Добавим в файл functions.php вашей темы или в отдельный плагин следующий код:

function wpcoding_register_event_post_type() {
    $labels = array(
        'name'               => 'События',
        'singular_name'      => 'Событие',
        'add_new'            => 'Добавить событие',
        'add_new_item'       => 'Добавить новое событие',
        'edit_item'          => 'Редактировать событие',
        'new_item'           => 'Новое событие',
        'view_item'          => 'Просмотр события',
        'search_items'       => 'Поиск событий',
        'not_found'          => 'События не найдены',
        'not_found_in_trash' => 'В корзине событий не найдено',
        'menu_name'          => 'События',
    );

    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'has_archive'        => true,
        'supports'           => array('title', 'editor', 'thumbnail'),
        'rewrite'            => array('slug' => 'events'),
        'show_in_rest'       => true,
    );

    register_post_type('wpcoding_event', $args);

    register_taxonomy('event_category', 'wpcoding_event', array(
        'label'        => 'Категории событий',
        'rewrite'      => array('slug' => 'event-category'),
        'hierarchical' => true,
        'show_in_rest' => true,
    ));
}
add_action('init', 'wpcoding_register_event_post_type');

Этот код создаст тип записи «Событие» с поддержкой категорий. Далее нужно добавить метаполе для даты события, например, с помощью Advanced Custom Fields (ACF) или вручную.

Добавление метаполя даты события вручную

Для простоты создадим метабокс даты события в админке:

function wpcoding_add_event_date_metabox() {
    add_meta_box(
        'wpcoding_event_date',
        'Дата события',
        'wpcoding_event_date_metabox_callback',
        'wpcoding_event',
        'side',
        'default'
    );
}
add_action('add_meta_boxes', 'wpcoding_add_event_date_metabox');

function wpcoding_event_date_metabox_callback($post) {
    wp_nonce_field('wpcoding_save_event_date', 'wpcoding_event_date_nonce');
    $value = get_post_meta($post->ID, '_wpcoding_event_date', true);
    echo '<input type="date" name="wpcoding_event_date" value="' . esc_attr($value) . '" />';
}

function wpcoding_save_event_date($post_id) {
    if (!isset($_POST['wpcoding_event_date_nonce']) || !wp_verify_nonce($_POST['wpcoding_event_date_nonce'], 'wpcoding_save_event_date')) {
        return;
    }
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (isset($_POST['wpcoding_event_date'])) {
        update_post_meta($post_id, '_wpcoding_event_date', sanitize_text_field($_POST['wpcoding_event_date']));
    }
}
add_action('save_post', 'wpcoding_save_event_date');

Теперь при создании события можно указать дату.

Вывод календаря с событиями на фронтенде

Создадим шорткод [wpcoding_events_calendar], который будет выводить календарь с событиями выбранного месяца.

function wpcoding_events_calendar_shortcode($atts) {
    $atts = shortcode_atts(array(
        'year'  => date('Y'),
        'month' => date('m'),
    ), $atts, 'wpcoding_events_calendar');

    ob_start();
    ?>
    <div id="wpcoding-events-calendar" data-year="<?php echo esc_attr($atts['year']); ?>" data-month="<?php echo esc_attr($atts['month']); ?>">
        <div class="wpcoding-calendar-controls">
            <button id="wpcoding-prev-month"><< Назад</button>
            <span id="wpcoding-current-month"><?php echo esc_html(date('F Y', strtotime($atts['year'] . '-' . $atts['month'] . '-01'))); ?></span>
            <button id="wpcoding-next-month">Вперед >></button>
        </div>
        <div id="wpcoding-calendar-days"></div>
        <div id="wpcoding-events-list"></div>
    </div>
    <?php
    return ob_get_clean();
}
add_shortcode('wpcoding_events_calendar', 'wpcoding_events_calendar_shortcode');

Этот шорткод создает контейнеры и кнопки для переключения месяцев. Теперь нужно реализовать AJAX-запросы для загрузки дней с событиями и списка событий.

AJAX-обработчик для загрузки данных календаря

Добавим обработчик, который по году и месяцу вернет массив дней с событиями и сами события:

function wpcoding_ajax_load_events() {
    $year = isset($_POST['year']) ? intval($_POST['year']) : date('Y');
    $month = isset($_POST['month']) ? intval($_POST['month']) : date('m');

    $start_date = "$year-$month-01";
    $end_date = date('Y-m-t', strtotime($start_date));

    $args = array(
        'post_type'      => 'wpcoding_event',
        'posts_per_page' => -1,
        'meta_query'     => array(
            array(
                'key'     => '_wpcoding_event_date',
                'value'   => array($start_date, $end_date),
                'compare' => 'BETWEEN',
                'type'    => 'DATE',
            ),
        ),
        'orderby'        => 'meta_value',
        'order'          => 'ASC',
        'meta_key'       => '_wpcoding_event_date',
    );

    $query = new WP_Query($args);
    $events = array();
    foreach ($query->posts as $post) {
        $date = get_post_meta($post->ID, '_wpcoding_event_date', true);
        if (!$date) continue;
        $day = date('j', strtotime($date));
        if (!isset($events[$day])) {
            $events[$day] = array();
        }
        $events[$day][] = array(
            'ID'    => $post->ID,
            'title' => get_the_title($post),
            'link'  => get_permalink($post),
        );
    }

    wp_send_json_success(array('events' => $events));
}
add_action('wp_ajax_wpcoding_load_events', 'wpcoding_ajax_load_events');
add_action('wp_ajax_nopriv_wpcoding_load_events', 'wpcoding_ajax_load_events');

JavaScript для вывода календаря и загрузки событий

Добавим JS, который будет строить сетку календаря и подгружать события.

document.addEventListener('DOMContentLoaded', function() {
    const calendar = document.getElementById('wpcoding-events-calendar');
    if (!calendar) return;

    let year = parseInt(calendar.dataset.year);
    let month = parseInt(calendar.dataset.month);

    const daysContainer = document.getElementById('wpcoding-calendar-days');
    const eventsList = document.getElementById('wpcoding-events-list');
    const currentMonthLabel = document.getElementById('wpcoding-current-month');
    const prevBtn = document.getElementById('wpcoding-prev-month');
    const nextBtn = document.getElementById('wpcoding-next-month');

    function loadEvents(year, month) {
        fetch(wpcoding_ajax_object.ajax_url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
            },
            body: new URLSearchParams({
                action: 'wpcoding_load_events',
                year: year,
                month: month
            })
        })
        .then(response => response.json())
        .then(data => {
            if (!data.success) return;
            renderCalendar(year, month, data.data.events);
        });
    }

    function renderCalendar(year, month, events) {
        daysContainer.innerHTML = '';
        eventsList.innerHTML = '';

        const firstDay = new Date(year, month - 1, 1).getDay();
        const daysInMonth = new Date(year, month, 0).getDate();

        // Корректируем для воскресенья
        const startDay = firstDay === 0 ? 7 : firstDay;

        // Выводим пустые ячейки для выравнивания
        for (let i = 1; i < startDay; i++) {
            const emptyCell = document.createElement('div');
            emptyCell.classList.add('calendar-day', 'empty');
            daysContainer.appendChild(emptyCell);
        }

        // Выводим дни месяца
        for (let day = 1; day <= daysInMonth; day++) {
            const dayCell = document.createElement('div');
            dayCell.classList.add('calendar-day');
            dayCell.textContent = day;

            if (events[day]) {
                dayCell.classList.add('has-event');
                dayCell.addEventListener('click', () => {
                    renderEventsList(day, events[day]);
                });
            }

            daysContainer.appendChild(dayCell);
        }

        currentMonthLabel.textContent = new Date(year, month - 1).toLocaleString('ru-RU', { month: 'long', year: 'numeric' });
    }

    function renderEventsList(day, events) {
        eventsList.innerHTML = '<h3>События ' + day + ' ' + currentMonthLabel.textContent + '</h3>';
        const ul = document.createElement('ul');
        events.forEach(event => {
            const li = document.createElement('li');
            const a = document.createElement('a');
            a.href = event.link;
            a.textContent = event.title;
            li.appendChild(a);
            ul.appendChild(li);
        });
        eventsList.appendChild(ul);
    }

    prevBtn.addEventListener('click', () => {
        month--;
        if (month < 1) {
            month = 12;
            year--;
        }
        loadEvents(year, month);
        eventsList.innerHTML = '';
    });

    nextBtn.addEventListener('click', () => {
        month++;
        if (month > 12) {
            month = 1;
            year++;
        }
        loadEvents(year, month);
        eventsList.innerHTML = '';
    });

    loadEvents(year, month);
});

Подключение стилей и скриптов

Добавьте в functions.php регистрацию и подключение скрипта с локализацией:

function wpcoding_enqueue_scripts() {
    wp_enqueue_script('wpcoding-events-calendar', get_template_directory_uri() . '/js/wpcoding-events-calendar.js', array(), '1.0', true);
    wp_localize_script('wpcoding-events-calendar', 'wpcoding_ajax_object', array(
        'ajax_url' => admin_url('admin-ajax.php'),
    ));

    wp_enqueue_style('wpcoding-events-calendar-style', get_template_directory_uri() . '/css/wpcoding-events-calendar.css');
}
add_action('wp_enqueue_scripts', 'wpcoding_enqueue_scripts');

Пример простых CSS стилей:

.calendar-day {
    display: inline-block;
    width: 40px;
    height: 40px;
    line-height: 40px;
    text-align: center;
    border: 1px solid #ddd;
    margin: 2px;
    cursor: default;
}
.calendar-day.has-event {
    background-color: #def;
    cursor: pointer;
}
.calendar-day.empty {
    border: none;
    background: transparent;
    cursor: default;
}
#wpcoding-events-calendar {
    max-width: 400px;
    margin: 0 auto;
}
.wpcoding-calendar-controls {
    text-align: center;
    margin-bottom: 10px;
}
#wpcoding-events-list {
    margin-top: 15px;
}
@media (max-width: 480px) {
    .calendar-day {
        width: 30px;
        height: 30px;
        line-height: 30px;
        font-size: 12px;
    }
}

Расширение функционала: фильтрация по категориям событий

Чтобы добавить фильтр по таксономии event_category, можно вывести select в шорткоде и передавать выбранную категорию в AJAX-запрос.

Пример добавления фильтра в шорткод:

function wpcoding_events_calendar_shortcode($atts) {
    $atts = shortcode_atts(array(
        'year'  => date('Y'),
        'month' => date('m'),
    ), $atts, 'wpcoding_events_calendar');

    $categories = get_terms(array(
        'taxonomy' => 'event_category',
        'hide_empty' => false,
    ));

    ob_start();
    ?>
    <div id="wpcoding-events-calendar" data-year="<?php echo esc_attr($atts['year']); ?>" data-month="<?php echo esc_attr($atts['month']); ?>">
        <select id="wpcoding-event-category-filter">
            <option value="">Все категории</option>
            <?php foreach ($categories as $cat) : ?>
                <option value="<?php echo esc_attr($cat->slug); ?>"><?php echo esc_html($cat->name); ?></option>
            <?php endforeach; ?>
        </select>
        <div class="wpcoding-calendar-controls">
            <button id="wpcoding-prev-month"><< Назад</button>
            <span id="wpcoding-current-month"><?php echo esc_html(date('F Y', strtotime($atts['year'] . '-' . $atts['month'] . '-01'))); ?></span>
            <button id="wpcoding-next-month">Вперед >></button>
        </div>
        <div id="wpcoding-calendar-days"></div>
        <div id="wpcoding-events-list"></div>
    </div>
    <?php
    return ob_get_clean();
}

И в JS добавить передачу категории в AJAX и обработчик изменения фильтра:

const categoryFilter = document.getElementById('wpcoding-event-category-filter');

function loadEvents(year, month, category = '') {
    const data = {
        action: 'wpcoding_load_events',
        year: year,
        month: month,
    };
    if (category) {
        data.category = category;
    }

    fetch(wpcoding_ajax_object.ajax_url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
        },
        body: new URLSearchParams(data)
    })
    .then(response => response.json())
    .then(data => {
        if (!data.success) return;
        renderCalendar(year, month, data.data.events);
    });
}

categoryFilter.addEventListener('change', () => {
    loadEvents(year, month, categoryFilter.value);
    eventsList.innerHTML = '';
});

В PHP-обработчике добавьте фильтрацию по категории:

if (!empty($_POST['category'])) {
    $args['tax_query'] = array(
        array(
            'taxonomy' => 'event_category',
            'field'    => 'slug',
            'terms'    => sanitize_text_field($_POST['category']),
        ),
    );
}

Вывод

Получился универсальный адаптивный календарь событий со своей логикой и поддержкой фильтров. Такой подход можно развивать: добавлять отображение подробностей события, интегрировать с плагинами WPShop (например, для событийных квизов Quizle или отзывов Expert Review), улучшать UX и производительность.

Этот пример отлично подходит для разработчиков, которые хотят контролировать каждый этап и оптимизировать под свои задачи, не нагружая сайт лишними плагинами.

×

AI-плагин от WPShop.ru

анализирует конкурентов

пишет статьи

готовит SEO

генерирует изображения

и еще кое-что...
WPGPT
Плагин, который наполняет ваш сайт WordPress
Узнать больше