C CoolAdmin v3.4.0

Calendar

CoolAdmin's calendar page uses FullCalendar 7.0.2 with four views (Month / Week / Day / List) and a self-updating event generator — every page load spreads ~20 events across the current month so the demo never goes stale.

Last updated August 3, 2026

The calendar page (calendar.html) is a working FullCalendar 7.0.2 instance with four built-in views (Month, Week, Day, List), event color-coding by tag, and a deadline list pane next to the grid.

The clever bit: events aren’t a static array. The page generates events relative to today on every load, so whether you open the demo in August 2026 or March 2030, the month always looks populated with past, present, and upcoming events.

Upgrading from CoolAdmin 3.3 or earlier? v3.4 moved from FullCalendar 6 to 7. The asset paths, the theming approach, and the per-event color keys all changed — see What changed in FullCalendar 7 at the bottom.

Where the code lives

FullCalendar loads on the calendar page only — not on every page in the template. The calendar’s init is an inline <script> at the bottom of calendar.html (generated from src/pug/partials/content/calendar.scripts.html).

v7 ships its CSS as real files, where v6 injected styles from JavaScript. That means two scripts and three stylesheets, and the order matters:

<!-- calendar.html — only this page loads FullCalendar -->
<!-- CSS order: skeleton → theme → palette (palette holds the color tokens) -->
<link rel="stylesheet" href="vendor/fullcalendar-7.0.2/skeleton.css">
<link rel="stylesheet" href="vendor/fullcalendar-7.0.2/themes/classic/theme.css">
<link rel="stylesheet" href="vendor/fullcalendar-7.0.2/themes/classic/palette.css">

<!-- …page markup… -->

<!-- Core bundle first — the theme self-registers into FullCalendar.Shared.globalPlugins -->
<script src="vendor/fullcalendar-7.0.2/fullcalendar.global.js"></script>
<script src="vendor/fullcalendar-7.0.2/themes/classic/theme.global.js"></script>

<script>
  document.addEventListener('DOMContentLoaded', function () {
    const calendarEl = document.getElementById('calendar');
    if (!calendarEl) return;

    const events = buildEvents(new Date());
    renderDeadlineList(events);

    const calendar = new FullCalendar.Calendar(calendarEl, {
      initialView: 'dayGridMonth',
      headerToolbar: {
        left:   'prev,next today',
        center: 'title',
        right:  'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
      },
      height:           'auto',
      events:           events,
      eventDisplay:     'block',
      displayEventTime: true,
      dayMaxEvents:     3,
      moreLinkText:     'more',
      navLinks:         true,
      nowIndicator:     true,
      eventTimeFormat:  { hour: 'numeric', minute: '2-digit', meridiem: 'short' },
      // Stable styling hooks — see "Theming" below
      dayHeaderInnerClass:  'ca-fc-day-header',
      dayCellTopInnerClass: 'ca-fc-day-number',
      eventClick: (info) => { /* …show a toast or open a detail modal… */ }
    });
    calendar.render();
    window.calendarInstance = calendar;
  });
</script>

The instance is parked on window.calendarInstance so other scripts can reach into it for calendar.addEvent(), calendar.refetchEvents(), etc.

No temporal-polyfill is required. FullCalendar 7 lists it as a peer dependency, but the global bundle carries its own Temporal shim and only uses the native globalThis.Temporal when the browser provides it. The peer dependency applies to bundler-based ESM consumers.

Theming

This is the biggest practical change in v7. FullCalendar now generates hashed internal class names (.fc-classic-dl6, .fc-classic-1Wx) that change between releases, so there is no stable class API to override. Selectors like .fc-daygrid-event or .fc-button-primary — which worked in v6 — match nothing in v7.

Instead, colors are driven by the theme’s public custom properties. CoolAdmin rebinds them to its own --m-* design tokens, which means the calendar automatically follows the theme switcher presets:

body.app {
  /* Toolbar buttons — flat, surface-colored, accent when active */
  --fc-classic-button:              var(--m-surface);
  --fc-classic-button-border:       var(--m-border);
  --fc-classic-button-foreground:   var(--m-text);
  --fc-classic-button-strong:       var(--m-accent);
  --fc-classic-button-strong-border: var(--m-accent);

  /* Primary / events */
  --fc-classic-primary:             var(--m-accent);
  --fc-classic-primary-foreground:  #fff;

  /* Calendar content */
  --fc-classic-today:               var(--m-accent-soft);
  --fc-classic-highlight:           rgba(var(--m-accent-rgb), 0.12);

  /* Neutrals — align the grid with card surfaces */
  --fc-classic-background:          var(--m-surface);
  --fc-classic-foreground:          var(--m-text);
  --fc-classic-border:              var(--m-border);
}

The full list of properties lives in vendor/fullcalendar-7.0.2/themes/classic/palette.css. That file also ships a [data-color-scheme=dark] block, so the calendar gets dark-mode tokens for free.

For anything that isn’t a color — typography, spacing — use the public class-name options rather than guessing at hashed selectors. CoolAdmin uses two:

dayHeaderInnerClass:  'ca-fc-day-header',   // uppercase SUN MON TUE headers
dayCellTopInnerClass: 'ca-fc-day-number',   // date number sizing
body.app .ca-fc-day-header {
  color: var(--m-text-faint);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

Because these class names are yours, they survive FullCalendar patch upgrades. Most render hooks have a matching *Class option — eventClass, dayCellClass, slotLabelClass, and so on.

The self-updating event generator

A buildEvents(today) function takes the current date and returns an array of ~20 events spread across that month — past, present, and future:

function buildEvents(today) {
  const y = today.getFullYear();
  const m = today.getMonth();

  // Helpers
  const ymd  = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  const ymdt = (d, h, mm) => ymd(d) + 'T' + `${String(h).padStart(2,'0')}:${String(mm||0).padStart(2,'0')}:00`;
  const offset  = (n) => { const d = new Date(today); d.setDate(today.getDate() + n); return d; };
  const nthDow  = (n, dow) => { /* nth occurrence of weekday `dow` in this month */ };
  const lastDow = (dow)    => { /* last occurrence of weekday `dow` in this month */ };

  return [
    // …weekly recurring items (weekday standups, every Friday coffee)
    // …date-relative anchors (offset(-5) for "five days ago", offset(+7) for "next week")
    // …calendar-anchored events (nthDow(1, 1) for "first Monday of the month")
  ];
}

Event color coding

Events are tagged by type, and each tag maps to a background/foreground pair:

const TAG_COLORS = {
  meeting:      { bg: '#eaf0fc', fg: '#4272d7' },  /* brand blue */
  task:         { bg: '#e0f3f1', fg: '#0d8780' },  /* teal */
  presentation: { bg: '#fff1e6', fg: '#d45f0a' },  /* warm orange */
  deadline:     { bg: '#fef2f2', fg: '#dc2626' },  /* red */
  personal:     { bg: '#fce7f3', fg: '#be185d' },  /* magenta */
};

Those get applied per event. v7 replaced v6’s backgroundColor / borderColor / textColor trio with color (fill) and contrastColor (text):

const e = (title, start, end, allDay, tag) => ({
  title,
  start,
  end,
  allDay,
  color:         TAG_COLORS[tag].bg,   // was backgroundColor + borderColor
  contrastColor: TAG_COLORS[tag].fg,   // was textColor
  extendedProps: { type: tag },
});

extendedProps carries per-event metadata that the click handler and the deadline list pane read for icons and labels. To add a tag, append to TAG_COLORS and pass the new key to e().

The deadline list pane

The right-side panel shows upcoming events in a sorted list, built by renderDeadlineList(events):

  • Filters events to those starting today or later
  • Sorts by start time
  • Takes the first 6
  • Renders each as a card with date, time, and title

This is a separate static render — it doesn’t update when the user navigates to a different month. For that, hook FullCalendar’s datesSet callback and re-render from calendar.getEvents().

Switching views

The header toolbar exposes four views:

Button View key What it shows
Month dayGridMonth 6-week grid (the default)
Week timeGridWeek Hour-granular columns for the week
Day timeGridDay Hour-granular column for one day
List listWeek Flat list of the week’s events

Change the initial view by editing initialView:

const calendar = new FullCalendar.Calendar(calendarEl, {
  initialView: 'timeGridWeek',   // ← was 'dayGridMonth'
  // …
});

Adding a real event source

The demo uses inline-generated events. To wire it to a backend, swap the events option to a function or URL:

const calendar = new FullCalendar.Calendar(calendarEl, {
  initialView: 'dayGridMonth',
  events: async (info, successCallback, failureCallback) => {
    try {
      const r = await fetch(`/api/events?start=${info.startStr}&end=${info.endStr}`);
      successCallback(await r.json());
    } catch (e) {
      failureCallback(e);
    }
  }
});

FullCalendar calls the function every time the visible date range changes. Your API returns events in the same shape: { title, start, end?, allDay?, color?, contrastColor?, extendedProps?: {} }.

See the FullCalendar event sources docs for the full schema.

What changed in FullCalendar 7

If you customized the calendar on CoolAdmin 3.3 or earlier, these are the breaking changes:

Area v6 v7
Assets one JS file, CSS injected by JS 2 scripts + 3 stylesheets
Theming override .fc-daygrid-event, .fc-button-primary, … --fc-classic-* custom properties
Class names stable and semantic hashed, change between releases
Event fill backgroundColor + borderColor color
Event text textColor contrastColor
Resize handling windowResize: () => calendar.updateSize() removed — handled internally
Custom classes CSS selectors dayHeaderInnerClass and friends

Any v6 .fc-* CSS you wrote will silently stop applying rather than erroring — if your calendar looks unstyled after upgrading, that’s why.

Why FullCalendar and not a custom calendar

FullCalendar gets four things almost for free:

  • All four views (Month / Week / Day / List) wired and styled
  • Navigation toolbar with prev/next/today and a view switcher
  • Click + drag interactions for resizing events and moving them between days
  • Accessibility — keyboard navigation, ARIA labels, focus management

Writing those from scratch is a multi-week project.

The cost is ~825 KB of FullCalendar assets, loaded only on the calendar page. That’s a one-page tax, not a template-wide cost — and if you don’t use the calendar, deleting vendor/fullcalendar-7.0.2/ reclaims all of it.

See also