Реализация календаря событий на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация календаря событий на сайте
Средняя
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация календаря событий на сайте

Календарь событий — сложнее, чем кажется. Повторяющиеся события, часовые пояса, перекрывающиеся слоты, мобильный UX, интеграция с Google Calendar — каждый пункт добавляет неделю к разработке. Разбираем по частям.

Выбор библиотеки

FullCalendar — промышленный стандарт. Поддерживает месяц/неделю/день/список, drag-and-drop, повторяющиеся события, iCalendar, Google Calendar API. Есть пакеты для React, Vue, Angular. Бесплатная версия закрывает 90% задач, Premium добавляет timeline и resource views.

React Big Calendar — легче, проще кастомизировать, меньше фич. Хорошо для простых расписаний.

TOAST UI Calendar — корейская разработка, хорошие возможности, активно развивается.

Для статического отображения (только вывод событий без интерактива) — можно обойтись без библиотек.

FullCalendar: базовая интеграция с React

npm install @fullcalendar/react @fullcalendar/core @fullcalendar/daygrid \
  @fullcalendar/timegrid @fullcalendar/list @fullcalendar/interaction
import FullCalendar from '@fullcalendar/react'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import listPlugin from '@fullcalendar/list'
import interactionPlugin from '@fullcalendar/interaction'
import { EventInput, DateSelectArg, EventClickArg } from '@fullcalendar/core'
import ruLocale from '@fullcalendar/core/locales/ru'

export function EventCalendar() {
  const [events, setEvents] = useState<EventInput[]>([])
  const [selectedEvent, setSelectedEvent] = useState<EventClickArg | null>(null)

  useEffect(() => {
    fetch('/api/events')
      .then(r => r.json())
      .then(data => setEvents(data.map(mapToFullCalendarEvent)))
  }, [])

  function handleEventClick(info: EventClickArg) {
    info.jsEvent.preventDefault()
    setSelectedEvent(info)
  }

  function handleDateSelect(info: DateSelectArg) {
    // Открываем форму создания события
    openCreateEventModal({ start: info.start, end: info.end, allDay: info.allDay })
  }

  return (
    <>
      <FullCalendar
        plugins={[dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin]}
        initialView="dayGridMonth"
        locale={ruLocale}
        headerToolbar={{
          left: 'prev,next today',
          center: 'title',
          right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek',
        }}
        buttonText={{
          today: 'Сегодня',
          month: 'Месяц',
          week: 'Неделя',
          day: 'День',
          list: 'Список',
        }}
        events={events}
        selectable={true}
        selectMirror={true}
        dayMaxEvents={3}        // "+N ещё" вместо переполнения
        weekends={true}
        eventClick={handleEventClick}
        select={handleDateSelect}
        eventTimeFormat={{ hour: '2-digit', minute: '2-digit', hour12: false }}
        firstDay={1}            // Неделя с понедельника
        height="auto"
        eventDisplay="block"
        eventContent={renderEventContent}
      />
      {selectedEvent && <EventModal event={selectedEvent} onClose={() => setSelectedEvent(null)} />}
    </>
  )
}

function renderEventContent(eventInfo: any) {
  return (
    <div className="fc-event-custom">
      <span className="fc-event-dot" style={{ background: eventInfo.event.backgroundColor }} />
      <span className="fc-event-title">{eventInfo.event.title}</span>
      {!eventInfo.event.allDay && (
        <span className="fc-event-time">{eventInfo.timeText}</span>
      )}
    </div>
  )
}

API: структура событий

// app/Http/Controllers/EventController.php
class EventController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $events = Event::query()
            ->when($request->start, fn($q) => $q->where('end_at', '>=', $request->start))
            ->when($request->end, fn($q) => $q->where('start_at', '<=', $request->end))
            ->with(['category', 'location'])
            ->get()
            ->map(fn(Event $e) => [
                'id'              => $e->id,
                'title'           => $e->title,
                'start'           => $e->start_at->toIso8601String(),
                'end'             => $e->end_at->toIso8601String(),
                'allDay'          => $e->all_day,
                'backgroundColor' => $e->category->color,
                'borderColor'     => $e->category->color,
                'url'             => route('events.show', $e),
                'extendedProps'   => [
                    'description' => $e->description,
                    'location'    => $e->location?->name,
                    'category'    => $e->category->name,
                ],
            ]);

        return response()->json($events);
    }
}

Повторяющиеся события через rrule

npm install @fullcalendar/rrule rrule
import rrulePlugin from '@fullcalendar/rrule'

// В массиве events:
const recurringEvent: EventInput = {
  id: 'weekly-standup',
  title: 'Еженедельный стендап',
  rrule: {
    freq: 'weekly',
    byweekday: ['mo', 'we', 'fr'],
    dtstart: '2025-01-01T09:00:00',
    until: '2025-12-31',
  },
  duration: '00:30',
  backgroundColor: '#0ea5e9',
}

// Исключения (отменённые даты)
const recurringWithExceptions: EventInput = {
  ...recurringEvent,
  exdate: [
    '2025-05-01T09:00:00',  // Праздник — пропускаем
    '2025-06-12T09:00:00',
  ],
}

Часовые пояса

// Хранить в UTC, отображать в часовом поясе пользователя
<FullCalendar
  timeZone="local"  // или конкретный: 'Europe/Moscow'
  // ...
/>

// Если события в разных TZ — конвертируем при загрузке
import { formatInTimeZone } from 'date-fns-tz'

function mapToFullCalendarEvent(event: ApiEvent): EventInput {
  return {
    ...event,
    start: formatInTimeZone(event.start_at, event.timezone, "yyyy-MM-dd'T'HH:mm:ss"),
    end: formatInTimeZone(event.end_at, event.timezone, "yyyy-MM-dd'T'HH:mm:ss"),
  }
}

Google Calendar интеграция

npm install @fullcalendar/google-calendar
import googleCalendarPlugin from '@fullcalendar/google-calendar'

<FullCalendar
  plugins={[dayGridPlugin, googleCalendarPlugin]}
  googleCalendarApiKey="AIza..."
  eventSources={[
    {
      googleCalendarId: '[email protected]',
      backgroundColor: '#4285f4',
      borderColor: '#4285f4',
    },
    {
      googleCalendarId: '[email protected]',
      backgroundColor: '#0f9d58',
      display: 'background',  // фоновая подсветка праздников
    },
  ]}
/>

Экспорт в iCalendar (.ics)

// Генерация .ics файла на Laravel
// composer require eluceo/ical

use Eluceo\iCal\Domain\Entity\Calendar;
use Eluceo\iCal\Domain\Entity\Event;
use Eluceo\iCal\Presentation\Factory\CalendarFactory;

class ICalExportController extends Controller
{
    public function export(Request $request): Response
    {
        $events = Event::whereHas('attendees', fn($q) => $q->where('user_id', auth()->id()))->get();

        $calendar = new Calendar(array_map(function (Event $e) {
            return (new \Eluceo\iCal\Domain\Entity\Event(
                new \Eluceo\iCal\Domain\ValueObject\UniqueIdentifier($e->uid)
            ))
            ->setSummary($e->title)
            ->setDescription($e->description ?? '')
            ->setOccurrence(
                new \Eluceo\iCal\Domain\ValueObject\TimeSpan(
                    new \Eluceo\iCal\Domain\ValueObject\DateTime(
                        \DateTimeImmutable::createFromMutable($e->start_at), true
                    ),
                    new \Eluceo\iCal\Domain\ValueObject\DateTime(
                        \DateTimeImmutable::createFromMutable($e->end_at), true
                    )
                )
            );
        }, $events->all()));

        return response(
            (new CalendarFactory())->createCalendar($calendar),
            200,
            [
                'Content-Type' => 'text/calendar; charset=UTF-8',
                'Content-Disposition' => 'attachment; filename="events.ics"',
            ]
        );
    }
}

Мобильный UX: swipe для смены месяца

// FullCalendar не поддерживает swipe нативно — добавляем через touch events
function addSwipeNavigation(calendarRef: React.RefObject<FullCalendar>) {
  let startX = 0

  const el = document.querySelector('.fc')!
  el.addEventListener('touchstart', (e: TouchEvent) => {
    startX = e.touches[0].clientX
  }, { passive: true })

  el.addEventListener('touchend', (e: TouchEvent) => {
    const diff = startX - e.changedTouches[0].clientX
    const api = calendarRef.current!.getApi()

    if (Math.abs(diff) > 50) {
      diff > 0 ? api.next() : api.prev()
    }
  }, { passive: true })
}

Сроки

Статичный календарь с выводом событий из API — 1 день. FullCalendar с месяц/неделя/список, модальным просмотром события и фильтром по категориям — 3–4 дня. С drag-and-drop редактированием, повторяющимися событиями, iCal-экспортом и Google Calendar синхронизацией — 2 недели.