Привет, Хабр! В свободное от работы время я занимаюсь разработкой своего проекта. На днях мне понадобилось разработать раздел с календарем и задачами, чтобы пользователи могли отслеживать свою деятельность. Увы, но полностью готовых решений я не нашел. API стандартного календаря Google не подходит, так как данные хочу хранить внутри контура проекта.
Спустя несколько часов поисков я наткнулся на плейлист разработчиков из Индии. В жизни все циклично — именно эти видео мне и помогли. Так я познакомился с классной open source-библиотекой FullCalendar, о которой расскажу в этой статье. Если вы уже начали составлять календари на 2025 год, добро пожаловать под кат!
Что за библиотека
FullCalendar — это open source-библиотека, которая позволяет создавать кастомные календари без лишних ручек по части бэкенда. Почти полностью фронтенд-решение, которое можно интегрировать без особых навыков. Есть версии как на чистом JavaScript, так и на React, Vue и старом добром Angular.
Главная страница FullCalendar.
Чем понравился FullCalendar
Есть два ограничения, которые я должен был учитывать при поиске решения. Первое: я не самый лучший фронтендер. Второе: мой проект построен на Django. Я убежден, что поиск решения — это всегда перебор разных вариантов, поэтому разводить лишний dependency hell из всяких React и Vue не хотелось.
Это open source
При интеграции вы получаете не какой-то iframe, а полноценный DOM-объект. Это значит, вы можете изменять CSS-стили для элементов календаря и обрабатывать события, связанные с ними. Это полезно, если нужно, например, показывать поп-апы по клику на конкретное задание.
Как я стилизовал стандартный шаблон под мобильный календарь.
Есть версия на нативном JavaScript
FullCalendar подкупил именно наличием версии из нативного JavaScript. Если не понравится, можно просто удалить link-импорты — и проект снова чистый. Но как это иногда бывает, любовь с первого взгляда превратилась в нечто большее.
Есть версия для бэкендеров с лапками
Нативный JavaScript — это хорошо. Но с учетом того, что и его я знаю так себе, не помешал бы Jquery. Разработчики FullCalendar предусмотрели и это. На каждый тип календаря, как я понял, есть две версии: расширенная (нативный JavaScript) и упрощенная (Jquery). К слову, версия с Jquery неплохая, для моего MVP подходит более чем.
Есть документация с песочницей
Очень порадовало, что на главном сайте библиотеки есть раздел Demos, в котором расположены самые популярные форматы календарей. Можно выбрать понравившийся, открыть его в Codepen и переиспользовать.
Раздел Demos с разными типами календарей.
Все — в JSON
Вам не нужно описывать логику генерации и отображения событий внутри календаря, все это сделано уже разработчиками. Просто даете ссылку на JSON-файл со списком событий, а дальше библиотека сама их разместит в колонках календаря. Требуемая структура описана в документации.
$(function() {
$('#calendar').fullCalendar({
defaultView: 'agendaWeek',
header: {
left: 'prev,next today',
center: 'title',
right: 'agendaWeek,agendaDay'
},
// вот эта ссылка может быть размещена локально или в вашем S3
events: 'https://fullcalendar.io/api/demo-feeds/events.json'
})
});
Как сделать свой календарь
Рассмотрим реализацию календаря, на примере самого классического шаблона, в котором уже реализована механика, приближенная к Google Calendar.
Шаг 1. Прикрутите фронтенд
Неважно, на чем у вас построено веб-приложение. Откройте шаблон вашего календаря и добавьте импорты, как в примере ниже:
<!-- calendar.html -->
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Мой календарь</title>
<link rel='stylesheet' href="./style.css">
<!-- стили календаря рекомендую хранить локально -->
<link rel='stylesheet' href="./fullcalendar.min.css">
</head>
<body>
<!-- тут что-то ваше –->
<!-- вот этот блок понадобится, это контейнер для календаря –->
<div id='calendar'></div>
<!-- тут что-то ваше –->
<script src='https://cdn.jsdelivr.net/npm/moment@2/min/moment.min.js'></script>
<script src='https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js'></script>
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@3.10.5/dist/fullcalendar.min.js'></script>
<!-- подгружаем главный скрипт, который все объединит –->
<script src="./calendar_loader.js"></script>
</body>
</html>
Далее создайте файл со стилями календаря и копируйте в него стандартный набор. Вместе с этим — заведите файл со скриптом загрузки календаря, он тоже должен быть размещен локально.
// calendar_loader.js
$(function() {
$('#calendar').fullCalendar({
defaultView: 'agendaWeek',
header: {
left: 'prev,next today',
center: 'title',
right: 'agendaWeek,agendaDay'
},
// https://fullcalendar.io/docs/events-json-feed
events: 'https://fullcalendar.io/api/demo-feeds/events.json'
})
});
Отлично! На этом этапе календарь уже отображается, пусть и с тестовым набором данных. Кстати, если у вас в календаре будут другие события, не удивляйтесь: разработчики периодически изменяют содержание тестового файла JSON.
Фронтенд FullCalendar с тестовыми данными.
Шаг 2. Настраиваем отображение
В шаблоне выше я рекомендую хранить fullcalendar.min.css локально. Во-первых, он так будет быстрее загружаться. Во-вторых, мы можем его видоизменять. В этом ничего сложного нет: просто откройте инспектор кода и посмотрите, к каким классам принадлежат элементы вашего календаря. Нужно поменять цвет события? Посмотрите класс в инспекторе — и замените background-color в fullcalendar.min.css. Никакого rocket science.
Шаг 3. Продумайте логику
Если у вас, как и у меня, многопользовательское приложение, продумайте логику: какие есть события, через какую форму пользователи будут их добавлять и так далее. Самый простой из вариантов логики может выглядеть так:
Схема хранения событий и подтягивания данных.
На самом деле, данные можно хранить любым способом. S3-хранилище мне нравится, потому что JSON-структуры сохраняются как объекты. Для меня это довольно удобно, т. к. в перспективе позволяет забыть о базах данных на десятки тысяч строк: пользователей и событий с каждым месяцем может быть все больше.
К тому же объектное хранилище выходит очень дешево, особенно если использовать холодный класс хранения. И последнее преимущество — возможность записи метаданных в объекты. В целом, это киллер-фича, которая позволяет записывать дополнительные сведения о событиях, не ограничиваясь стандартной структурой JSON для работы FullCalendar.
Выбор хранилища зависит от вас и вводных. Например, если есть события, которые встречаются у разных пользователей (это могут быть общие встречи), то такие данные лучше грузить в базу данных. Так получится избежать дублей.
Шаг 4. Реализуйте логику
Рассмотрим на примере предложенной логики.
1. Создайте контейнер объектного хранилища S3. Это можно сделать, например, в облаке Selectel. Для этого перейдите в панель управления my.selectel.ru и откройте раздел Объектное хранилище. Нажмите Создать контейнер, введите его название, выберите тип и класс хранения. Для простоты эксперимента создадим публичный контейнер — не делайте так, если заботитесь о конфиденциальности событий.
Страница создания контейнера в панели управления.
2. Создайте сервисного пользователя в разделе Управление доступом → Сервисные пользователи, нажав на кнопку Добавить пользователя. После сгенерируйте для него S3-ключи — они понадобятся для подключения к объектному хранилищу из бэкенда вашего приложения. Так что сохраните их после получения.
Генерация S3-ключей для сервисного пользователя.
3. Подключитесь к контейнеру из своего приложения и создайте объект с JSON-структурой событий пользователя. Как это сделать с помощью S3-API, рассказали в документации.
# calendar_generator.py
# импорт boto3
import boto3
# ключи генерируем в разделе сервисных пользователей
# (замените на свои, эти — для примера)
access_key = "914866b003e94c569f7b5beb19cb0adf"
secret_key = "70232d4de75c4877916d0998db78dbd0"
# указываем типовой ендпоинт, название региона, access_key и aws_secret_access_key
s3 = boto3.client("s3", endpoint_url="https://s3.ru-1.storage.selcloud.ru",
region_name="ru-1", aws_access_key_id=access_key, aws_secret_access_key=secret_key,
verify=False)
# пользовательские данные (для примера)
users_calendars = {
'user_1': {
'09.12.24-13.12.24' : '[{"title":"Lunch","start":"2024-12-09T12:00:00+00:00"}]'
}
}
# название контейнера до первой точки
# (можно посмотреть в панели управления)
bucket_name = 'fullcalendar'
# список пользователей и дат
users_list = list(users_calendars.keys())
dates_list = list(users_calendars["user_1"].keys())
# название объекта, в который мы запишем данные (наш json-файл)
# учтите: если хотите расположить внутри директории,
# пропишите ее на уровне названия объекта — она появится
key = f'{users_list[0]}/{dates_list[0]}.json'
# данные, которые запишем внутрь объекта (тело объекта)
body = users_calendars[users_list[0]][dates_list[0]]
# загрузка объекта из строки
s3.put_object(Bucket=bucket_name, Key=key, Body=body)
Отлично! Мы подгрузили «откуда-то» данные о событиях пользователя user-1 на рабочей неделе с 9 по 13 декабря. Теперь при переходе в S3-хранилище вы увидите директорию и подгруженный объект:
4. Внутри fullcalendar.min.js замените адрес events на уникальную ссылку с событиями пользователя:
// calendar_loader.js
$(function() {
...
// теперь здесь подгружаем ссылку на объект в s3
// ссылку можно скопировать из панели управления
events: 'https://f7f2013c-d34e-4560-99c6-5a60b6d5b291.selstorage.ru/user_1/09.12.24-13.12.24.json'
})
});
Готово — все события отображаются в календаре.
При необходимости вы можете его стилизовать и, например, синхронизировать с другими календарями с помощью протокола CalDAV. Но это уже другая история.