Реалізація календаря подій на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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

Реалізація календаря подій на вебсайті

Календар подій складніший, ніж здається. Повторювані eventos, часові пояси, перекриваючі слоти, мобільний 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 ukLocale from '@fullcalendar/core/locales/uk'

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={ukLocale}
        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/Kyiv'
  // ...
/>

// Якщо события в різних 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: свайп для зміни місяця

// 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 тижні.