templates/public/events/_calendar.html.twig line 1

  1. <style>
  2. .fc-tooltip {
  3.   position: absolute;
  4.   background: #ffffff;
  5.   border: 1px solid var(--color-border-light);
  6.   padding: 10px 12px;
  7.   border-radius: 8px;
  8.   box-shadow: 0 6px 18px rgba(0,0,0,0.15);
  9.   font-size: 13px;
  10.   z-index: 9999;
  11.   max-width: 240px;
  12. }
  13. .fc-tooltip a {
  14.   display: inline-block;
  15.   margin-top: 6px;
  16.   font-weight: 600;
  17.   text-decoration: none;
  18.   color: var(--color-course-icon);
  19. }
  20. .fc-h-event.fc-event-open,
  21. .fc-event.fc-event-open,
  22. .fc-event-open .fc-event-main {
  23.   background: #198754 !important;
  24.   border-color: #198754 !important;
  25. }
  26. .fc-h-event.fc-event-muted,
  27. .fc-event.fc-event-muted,
  28. .fc-event-muted .fc-event-main {
  29.   background: #dfe3e8 !important;
  30.   border-color: #dfe3e8 !important;
  31.   color: #5f6b7a !important;
  32. }
  33. .fc-daygrid-more-link {
  34.   color: var(--color-primary) !important;
  35.   font-weight: 600;
  36. }
  37. .fc .fc-col-header-cell-cushion {
  38.   display: block;
  39.   padding: 8px 4px;
  40.   white-space: nowrap;
  41.   text-transform: capitalize;
  42.   font-size: 0.92rem;
  43. }
  44. .fc .fc-toolbar-title {
  45.   font-size: 1.2rem;
  46. }
  47. @media (max-width: 768px) {
  48.   .calendar-wrapper {
  49.     padding: 12px 8px;
  50.   }
  51.   .fc .fc-toolbar.fc-header-toolbar {
  52.     gap: 8px;
  53.     margin-bottom: 0.85rem;
  54.   }
  55.   .fc .fc-toolbar-title {
  56.     font-size: 1rem;
  57.   }
  58.   .fc .fc-button {
  59.     padding: 0.34rem 0.58rem;
  60.     font-size: 0.8rem;
  61.   }
  62.   .fc .fc-col-header-cell-cushion {
  63.     padding: 6px 2px;
  64.     font-size: 0.74rem;
  65.     letter-spacing: 0.01em;
  66.   }
  67. }
  68. @media (max-width: 576px) {
  69.   .fc .fc-col-header-cell-cushion {
  70.     padding: 6px 1px;
  71.     font-size: 0.7rem;
  72.   }
  73. }
  74. </style>
  75. <div class="calendar-wrapper">
  76.   <div id="calendar"></div>
  77. </div>
  78. <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js"></script>
  79. <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/locales-all.global.min.js"></script>
  80. <script>
  81. document.addEventListener('DOMContentLoaded', function () {
  82.   const calendarEl = document.getElementById('calendar');
  83.   let tooltip = null;
  84.   if (!calendarEl) return;
  85.   const mobileBreakpoint = window.matchMedia('(max-width: 576px)');
  86.   const tabletBreakpoint = window.matchMedia('(max-width: 768px)');
  87.   function getResponsiveCalendarOptions() {
  88.     if (mobileBreakpoint.matches) {
  89.       return {
  90.         dayHeaderFormat: { weekday: 'narrow' },
  91.         aspectRatio: 0.78,
  92.         dayMaxEventRows: 1,
  93.       };
  94.     }
  95.     if (tabletBreakpoint.matches) {
  96.       return {
  97.         dayHeaderFormat: { weekday: 'short' },
  98.         aspectRatio: 1.0,
  99.         dayMaxEventRows: 1,
  100.       };
  101.     }
  102.     return {
  103.       dayHeaderFormat: { weekday: 'long' },
  104.       aspectRatio: 1.2,
  105.       dayMaxEventRows: 2,
  106.     };
  107.   }
  108.   const calendar = new FullCalendar.Calendar(calendarEl, {
  109.     initialView: 'dayGridMonth',
  110.     height: 'auto',
  111.     firstDay: 1,
  112.     locale: 'es',
  113.     fixedWeekCount: false,
  114.     dayMaxEvents: true,
  115.     dayMaxEventRows: getResponsiveCalendarOptions().dayMaxEventRows,
  116.     moreLinkText: function (n) {
  117.     return '+' + n + ' más';
  118.     },
  119.     aspectRatio: getResponsiveCalendarOptions().aspectRatio,
  120.     headerToolbar: {
  121.       left: 'prev,next today',
  122.       center: 'title',
  123.       right: ''
  124.     },
  125.     buttonText: {
  126.       today: 'Hoy'
  127.     },
  128.     dayHeaderFormat: getResponsiveCalendarOptions().dayHeaderFormat,
  129.     eventClassNames: function (info) {
  130.       if (info.event.extendedProps.statusKey === 'open') {
  131.         return ['fc-event-open'];
  132.       }
  133.       return ['fc-event-muted'];
  134.     },
  135.     events: [
  136.       {% for e in events %}
  137.       {% set today = "now"|date("Y-m-d") %}
  138.       {% set ini = e.inscripcionesInicio ? e.inscripcionesInicio|date("Y-m-d") : null %}
  139.       {% set fin = e.inscripcionesFin ? e.inscripcionesFin|date("Y-m-d") : null %}
  140.       {% set inscriptionStatus = '' %}
  141.       {% set statusKey = 'open' %}
  142.       {% if ini and fin %}
  143.         {% if today >= ini and today <= fin %}
  144.           {% set inscriptionStatus = 'Inscripciones abiertas' %}
  145.           {% set statusKey = 'open' %}
  146.         {% elseif today < ini %}
  147.           {% set inscriptionStatus = 'Próximamente' %}
  148.           {% set statusKey = 'upcoming' %}
  149.         {% else %}
  150.           {% set inscriptionStatus = 'Inscripciones cerradas' %}
  151.           {% set statusKey = 'closed' %}
  152.         {% endif %}
  153.       {% endif %}
  154.       {
  155.         title: '{{ e.name|e("js") }}',
  156.         start: '{{ e.startDate|date("Y-m-d") }}',
  157.         end: '{{ e.endDate|date("Y-m-d") }}',
  158.         url: '{{ path("events_public_ficha", { extCode: e.extCode }) }}',
  159.         extendedProps: {
  160.           modality: '{{ e.modality ? e.modality.label|e("js") : "" }}',
  161.           inscriptionStatus: '{{ inscriptionStatus|e("js") }}',
  162.           statusKey: '{{ statusKey }}'
  163.         }
  164.       },
  165.       {% endfor %}
  166.     ],
  167.     eventMouseEnter: function (info) {
  168.       const event = info.event;
  169.       tooltip = document.createElement('div');
  170.       tooltip.className = 'fc-tooltip';
  171.       tooltip.innerHTML = `
  172.         <strong>${event.title}</strong><br>
  173.         ${event.extendedProps.modality || ''}<br>
  174.         ${event.extendedProps.inscriptionStatus || ''}
  175.       `;
  176.       document.body.appendChild(tooltip);
  177.       const rect = info.el.getBoundingClientRect();
  178.       tooltip.style.left =
  179.         rect.left + window.scrollX + 'px';
  180.       tooltip.style.top =
  181.         rect.top + window.scrollY - tooltip.offsetHeight - 8 + 'px';
  182.     },
  183.     eventMouseLeave: function () {
  184.       if (tooltip) {
  185.         tooltip.remove();
  186.         tooltip = null;
  187.       }
  188.     }
  189.   });
  190.   function applyResponsiveOptions() {
  191.     const responsiveOptions = getResponsiveCalendarOptions();
  192.     calendar.batchRendering(function () {
  193.       calendar.setOption('dayHeaderFormat', responsiveOptions.dayHeaderFormat);
  194.       calendar.setOption('aspectRatio', responsiveOptions.aspectRatio);
  195.       calendar.setOption('dayMaxEventRows', responsiveOptions.dayMaxEventRows);
  196.     });
  197.   }
  198.   function bindMediaQueryChange(mediaQuery, handler) {
  199.     if (typeof mediaQuery.addEventListener === 'function') {
  200.       mediaQuery.addEventListener('change', handler);
  201.       return;
  202.     }
  203.     if (typeof mediaQuery.addListener === 'function') {
  204.       mediaQuery.addListener(handler);
  205.     }
  206.   }
  207.   bindMediaQueryChange(mobileBreakpoint, applyResponsiveOptions);
  208.   bindMediaQueryChange(tabletBreakpoint, applyResponsiveOptions);
  209.   calendar.render();
  210.   applyResponsiveOptions();
  211. });
  212. </script>