Календарь событий — очень востребованный элемент для многих сайтов на 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 и производительность.
Этот пример отлично подходит для разработчиков, которые хотят контролировать каждый этап и оптимизировать под свои задачи, не нагружая сайт лишними плагинами.