LC Event Calendar self-updates
Events/Workshops What's On calendar plugin for WordPress.
by Loose Connections · github.com/loose-connections/lc-event-calendar · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/loose-connections/lc-event-calendar/archive/refs/heads/main.zipShips its own WordPress updater (built-in updater), so new versions show up under Dashboard → Updates.
A combined "What's On" calendar for Events and Workshops, built by Loose Connections as a reusable, installable WordPress plugin (not a set of code snippets). Originally developed for a client site as loose PHP snippets; rebuilt here as a proper plugin so it can be installed on any site.
What this plugin does
A single "What's On" page shows a calendar (month grid + list view toggle) combining two custom post types, event and workshop. Clicking a date shows a hover tooltip with the title, a short description and a featured image; clicking navigates straight to that post. Each individual event/workshop post can also display its own dates directly on its own page (next date, full list of upcoming dates, or a plain-text recurrence rule), independently of the shared calendar.
Workshops support three different ways of being scheduled, so staff never have to manually re-date or republish a recurring workshop:
- Specific dates - one or more individually chosen date/time pairs (e.g. a term's worth of sessions).
- Repeats weekly - runs on one or more days of the week, indefinitely or until a set end date.
- Repeats monthly - runs on a pattern like "the first Saturday of every month".
Events only ever have a single start/end date pair.
Requirements
- WordPress 6.0+, PHP 7.4+.
- ACF Pro (Advanced Custom Fields Pro) active - required for the Repeater field used by "Specific dates" workshops. The plugin shows an admin notice if this is missing.
- Elementor, or any page builder/theme, for placing the shortcodes. Nothing here depends on Elementor specifically.
What's different from a snippets-based build
This plugin is fully self-contained:
- The
eventandworkshoppost types are registered by the plugin (includes/class-post-types.php). If a site already has these post types registered elsewhere, addadd_filter( 'lc_event_calendar_skip_post_type_registration', '__return_true' );beforeinitto stop the plugin registering its own. - Every ACF field is registered in PHP (
includes/class-acf-fields.php) viaacf_add_local_field_group(). There is no JSON file to import through Custom Fields -> Tools; the fields exist as soon as the plugin is active. - All field names are prefixed
lc_to avoid clashing with anything else on a site. - Colours are filterable, not hardcoded, so the plugin can be re-themed per site (see Theming below) rather than carrying one client's brand colours.
- CSS and JS live in real asset files (
assets/css/,assets/js/) rather than being built as PHP strings, which also removes the "PHP interpolating JS as a variable" failure mode entirely (see Known gotchas below).
Installation
- Make sure ACF Pro is installed and active.
- Install the plugin (upload the zip via Plugins -> Add New -> Upload Plugin, or unzip into
/wp-content/plugins/). - Activate LC Event Calendar. This registers the Event and Workshop post types and every ACF field automatically - nothing to import.
- Test the REST endpoint by visiting
/wp-json/lc/v1/eventsdirectly in a browser; it should return a JSON array (empty until you add some events/workshops). - Add a page for your calendar and place
[lc_calendar]on it. - On the single Event/Workshop template, add whichever of
[lc_next_date],[lc_schedule_rule],[lc_upcoming_dates]or[lc_upcoming_events]you want, in whatever layout suits the site. - Add events and workshops. Set each event's Start Date/End Date. Set each workshop's Schedule Type and fill in the fields that appear (see the team guide below).
- Make sure every event/workshop has a Featured Image set, since it's used in the calendar hover tooltip.
- Test one of each schedule type (a specific-dates workshop, a weekly one, a monthly one if used, and a plain event) on both the calendar and the single-post shortcodes.
Auto-updates
This plugin isn't on WordPress.org, so without any extra setup, updating it on a live site means deactivating, deleting and re-uploading a new zip every time, and losing update notices entirely. includes/class-updater.php fixes that using WordPress's own "Update URI" plugin header (built into core since WP 5.8), pointed at a GitHub repo's Releases. No bundled third-party library, and nothing calls out to anywhere except GitHub's API.
Once it's set up, a site with this plugin installed shows a normal "Update available" notice on the Plugins page, with a one-click update, exactly like a WordPress.org plugin.
One-time setup:
- Create a GitHub repository for this plugin (public is simplest; see "Private repos" below if it needs to stay private).
- Push this plugin's code to it (
git init,git add .,git commit, add the GitHub remote,git push). - Open
includes/class-updater.phpand change the default inrepo_slug()from'your-org/lc-event-calendar'to your realowner/repo(e.g.'loose-connections/lc-event-calendar'). Commit and push that change too. - Install this build on each client site once, the normal way (upload the zip, activate). From here on, updates are automatic.
Shipping a new version, from then on:
- Bump the
Versionheader inlc-event-calendar.php(andLC_EVENT_CALENDAR_VERSION), commit, push. - Build the zip the same way as always (zip the
lc-event-calendarfolder). - On GitHub, create a Release with a tag matching the version (
1.0.2, orv1.0.2- both work), and attach the built zip to the release as an asset. This last step matters: the updater specifically looks for an attached.zipfile, because its internal folder is already namedlc-event-calendarand installs cleanly. A release with no attached zip is ignored (sites simply won't see an update for it), rather than risking a broken auto-generated archive. - Sites pick up the new version within the cache window (up to 12 hours), or immediately if someone clicks "Check again" on the Plugins page or visits Dashboard -> Updates.
Private repos: if the repo can't be public, generate a fine-grained GitHub personal access token with read-only access to that repo's contents, and set it per site via the lc_event_calendar_github_token filter (in a small site-specific snippet or mu-plugin, never committed into this plugin's own repo):
add_filter( 'lc_event_calendar_github_token', function () {
return 'github_pat_xxxxxxxxxxxx';
} );
Theming / colours
Colours default to a neutral palette and are filterable so each site can re-theme the calendar without editing plugin files. Add this to the theme's functions.php, or a snippet:
add_filter( 'lc_event_calendar_colors', function ( $colors ) {
return array(
'event' => '#68003B', // Event colour
'workshop' => '#E35000', // Workshop colour, today highlight
'text' => '#161616', // Body/heading text, weekday header background
'light_bg' => '#F1F1F1', // Card/list-item backgrounds
'border' => '#ECECEC',
'hover' => '#840157', // Button hover state
'today_tint' => 'rgba(227, 80, 0, 0.08)',
);
} );
Any key you leave out keeps its default.
Shortcodes reference
| Shortcode | Where to use | What it shows |
|---|---|---|
[lc_calendar] |
Once, on the main What's On page | Full FullCalendar month grid + list view toggle |
[lc_schedule_rule] |
On a single event/workshop page | Plain-text recurrence pattern (e.g. "Every Tuesday" / "2:00PM - 4:00PM" / date range). Only outputs anything for Repeats weekly / Repeats monthly workshops |
[lc_next_date] |
On a single event/workshop page | Just the single next upcoming date, plain text (no box), same styling as lc_schedule_rule |
[lc_upcoming_dates limit="6"] |
On a single event/workshop page | Full boxed list of every upcoming date, one by one. Works for events and any workshop schedule type |
[lc_upcoming_events limit="6"] |
On a single event/workshop page | Functionally identical to lc_upcoming_dates, just a separate tag name so it can be used independently |
All five accept an optional post_id attribute; without it they default to whichever post the shortcode sits on (works correctly inside a shared single template applied across every event/workshop).
A typical single-post layout: [lc_next_date] near the top as a highlight, [lc_schedule_rule] beneath it if it's a recurring workshop, then [lc_upcoming_dates] or [lc_upcoming_events limit="10"] further down for the full list.
Day-of-week numbering convention
This build deliberately uses Monday = 1 ... Sunday = 7 everywhere (matching PHP's native DateTime::format('N')), not the ACF/JavaScript default of Sunday = 0 ... Saturday = 6. This applies to both the "Days of the Week" checkbox and "Which Day" select on the Workshop Schedule fields. The only place a conversion to the JS/FullCalendar Sunday = 0 convention happens is inside the REST endpoint when building the daysOfWeek array for FullCalendar (via % 7) - don't change this convention without also updating that line.
The Schedule Type field stores plain lowercase text, never numbers: exactly one of dates, recurring_weekly, or recurring_monthly.
Team quick-reference (for whoever adds workshop dates day to day)
A plain-English guide for non-technical staff adding/updating workshops, no code involved.
Which Schedule Type to pick:
- Specific dates - for hand-picked individual dates (a term's worth of sessions, a one-off run, anything irregular). Safe default if unsure.
- Repeats weekly - runs the same day(s) every week, indefinitely or until a known end date.
- Repeats monthly - runs on a pattern like "the first Saturday of every month".
Specific dates: Set Schedule Type, then under Session Dates click "Add Date" and fill in Start/End for each session. Add more rows any time, including for future terms, on the same post - no need to create a new post.
Repeats weekly: Set Schedule Type, tick every day it runs on, set Start Time/End Time, set Recurring From, leave Recurring Until blank unless there's a known end date. It then keeps appearing automatically, nothing further to do.
Repeats monthly: Same as weekly, but set Which Week (First/Second/Third/Fourth/Last) and Which Day instead of ticking days.
General notes: Changing Schedule Type just switches which fields are shown; it doesn't delete data in the other sections, so it's safe to try a different option and switch back. If a workshop stops running, unpublish/trash the post as normal, no other cleanup needed. This only applies to Workshops - Events keep using the simple single start/end date fields since they're normally one-off.
Known gotchas (do not reintroduce)
- Repeater raw-vs-formatted bug:
get_field('lc_workshop_sessions', $post_id, false)(the "raw" third argument) returns each repeater row keyed by internal field key instead of field name, which silently breaks any code expecting name-based keys. Always fetchlc_workshop_sessionsWITHOUT thefalseargument (default, formatted mode). ACF'sdate_time_pickerreturn format is alreadyY-m-d H:i:s, so formatted mode gives the same precision as raw would, with correct array keys. - JS-in-PHP-string interpolation: the original snippets-based build kept the calendar's JavaScript inside a PHP double-quoted string via
wp_add_inline_script(), where a literal$followed by{or a variable-looking name would silently break the snippet through PHP variable interpolation. This plugin avoids that failure mode entirely by keeping the JS as a real static file (assets/js/calendar.js) with no PHP inside it at all; per-site values (the REST URL, colours) are passed in viawp_localize_script()instead. If this file is ever edited, there's simply no PHP-interpolation risk to worry about - but don't reintroduce inlinewp_add_inline_script()PHP-string JS without the same caution. - FullCalendar v6 button spacing: use
margin-inline-start, notmargin-left, when spacing toolbar buttons - FullCalendar v6 uses the logical CSS property for RTL support. - Toggle button "jump" on click: always give every button state (active and inactive) the same border width (
1px solid transparentas baseline) so nothing shifts by a couple of pixels on click. - Tooltip image not full width: WordPress's theme-level
img { max-width:100%; height:auto; }reset overrides custom tooltip image sizing unless!importantis added towidth,max-widthandheighton.lc-event-tooltip img.
File structure
lc-event-calendar/
├── lc-event-calendar.php Main plugin file (header, bootstrap, activation hooks)
├── includes/
│ ├── class-post-types.php Registers 'event' and 'workshop' post types
│ ├── class-acf-fields.php Registers every ACF field group in PHP
│ ├── class-rest-api.php GET /wp-json/lc/v1/events
│ ├── class-calendar.php FullCalendar enqueue, colours, [lc_calendar]
│ ├── class-shortcodes.php The four per-post shortcodes
│ └── class-updater.php Self-hosted auto-updates via GitHub Releases
├── assets/
│ ├── css/
│ │ ├── calendar.css Calendar grid/list styling + tooltip
│ │ └── shortcodes.css Per-post shortcode styling
│ └── js/
│ └── calendar.js FullCalendar init, fetch, tooltip, click-through
├── readme.txt Standard WordPress plugin readme
└── README.md This file
Brand colours used by default
| Name | Hex | Used for |
|---|---|---|
| Event colour | #2563EB |
Event colour, button theme |
| Workshop colour | #F97316 |
Workshop colour, today highlight, active toggle |
| Text | #111827 |
Body/heading text, weekday header background |
| Light | #F3F4F6 |
Card/list-item backgrounds |
| Hover | #1D4ED8 |
Button hover state |
Override any of these per site with the lc_event_calendar_colors filter (see Theming above).