Add event contact email and multi-day occurrences design specifications
- Introduced a new specification for adding an optional contact email field to events, including front-end output and compatibility testing. - Added a comprehensive design document for multi-day event occurrences, detailing the new occurrence data model, validation, normalization, and calendar architecture. - Created specifications for ACF field-key switching compatibility and local ACF override compatibility to ensure seamless integration with existing themes. - Implemented a desktop calendar view toggle design, allowing users to switch between calendar and list views, with persistent preferences and responsive behavior.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Event Contact Email Design
|
||||
|
||||
## Objective
|
||||
|
||||
Add an optional contact email to event editing and display it as a clickable email link on single-event pages only.
|
||||
|
||||
## Field Model
|
||||
|
||||
The built-in Event Details ACF group adds an optional field with:
|
||||
|
||||
- Label: `Contact Email`
|
||||
- Name: `event_contact_email`
|
||||
- Type: `email`
|
||||
|
||||
The field is stored at event level because it applies to the event rather than an individual occurrence. Admin-created override groups may add a field with the same name to use the same front-end output.
|
||||
|
||||
## Front-End Output
|
||||
|
||||
`templates/single-event.php` adds a Contact Email row within the existing event details. The row is omitted when the value is empty or invalid. Valid addresses render as a `mailto:` link. The visible address is protected with WordPress's `antispambot()` helper, and the address and URL are escaped for their output contexts.
|
||||
|
||||
Archive cards and calendar views do not display the contact email.
|
||||
|
||||
## Compatibility and Testing
|
||||
|
||||
Existing events require no migration because the field is optional. Tests verify the built-in field registration contract and the single-event template's conditional, sanitized email output. PHP syntax and the complete test suite must pass.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Contact names, phone numbers, or reusable contact records.
|
||||
- Sending email through WordPress.
|
||||
- Displaying contact email on archives or calendars.
|
||||
@@ -0,0 +1,153 @@
|
||||
# Multi-Day Event Occurrences Design
|
||||
|
||||
## Objective
|
||||
|
||||
Add an intuitive event-occurrence model that supports single-day events, continuous multi-day events, recurring events, and combinations of those patterns. Display multi-day occurrences as continuous bars across a traditional month-grid calendar and use a chronological agenda on phone-sized screens.
|
||||
|
||||
## Existing Behavior
|
||||
|
||||
The plugin stores discrete dates in the optional `event_dates` ACF repeater and falls back to the legacy `event_datetime` field. Calendar rendering places the event independently inside each matching day cell. Treating a multi-day event as several discrete dates therefore produces repeated links rather than one visually continuous event.
|
||||
|
||||
## Occurrence Data Model
|
||||
|
||||
Keep the `event_dates` field name for storage compatibility, but change its editor label to **Occurrences**. Each repeater row represents one occurrence and contains:
|
||||
|
||||
- `date`: required start date in `Y-m-d` format.
|
||||
- `start_time`: optional start time.
|
||||
- `end_date`: optional end date in `Y-m-d` format.
|
||||
- `end_time`: optional end time.
|
||||
- `all_day`: boolean toggle.
|
||||
|
||||
An omitted `end_date` means the occurrence ends on its start date. An `end_time` without an `end_date` therefore applies to the start date. When `all_day` is enabled, the time controls are hidden and stored time values are ignored by readers.
|
||||
|
||||
Every row is independent. A single event may contain any mixture of single-day and multi-day occurrences, allowing clients to represent recurrence and duration together.
|
||||
|
||||
### Validation
|
||||
|
||||
- A start date is required for every row.
|
||||
- An end date cannot precede the start date.
|
||||
- When both times apply to the same date, the end time cannot precede the start time.
|
||||
- Time comparison uses the WordPress site timezone.
|
||||
- Missing optional end values are valid and must not prevent saving.
|
||||
|
||||
Validation messages identify the affected occurrence row and field. Invalid ranges are rejected rather than silently reordered.
|
||||
|
||||
## Normalization and Compatibility
|
||||
|
||||
Introduce a normalized occurrence helper as the sole read interface used by plugin templates and admin features. Each normalized item contains the post ID, start date/time, effective end date/time, all-day status, and whether the start or end was inferred from legacy data.
|
||||
|
||||
The read priority is:
|
||||
|
||||
1. Rows in `event_dates`.
|
||||
2. The legacy `event_datetime` field.
|
||||
3. Older standalone `date` and `start_time` fields, where present.
|
||||
|
||||
Existing `event_dates` rows remain valid because all new subfields are optional. When an editor saves an event that has no repeater rows but does have legacy date data, the plugin creates one equivalent occurrence row. Untouched events continue to render through the read fallback, so migration does not require a bulk operation.
|
||||
|
||||
The legacy fields remain registered for compatibility but should be visually de-emphasized in the built-in editor once occurrence editing is available. Migration must be idempotent and must never append a duplicate row on subsequent saves.
|
||||
|
||||
Sites using their own admin-created ACF event field group are responsible for adding the new optional subfields with the documented names. Normalization tolerates absent subfields.
|
||||
|
||||
## Editor Experience
|
||||
|
||||
The built-in ACF repeater is labeled **Occurrences**, with an **Add Occurrence** button. Rows expose start and end values together so editors understand that a row is one continuous occurrence rather than one day.
|
||||
|
||||
The `all_day` toggle conditionally hides `start_time` and `end_time`. The end controls remain optional. Concise field instructions explain that additional rows create recurrence and that an end date creates a continuous multi-day occurrence.
|
||||
|
||||
## Calendar Architecture
|
||||
|
||||
Desktop and tablet retain a traditional month grid: seven weekday columns, date-numbered cells, and one row per calendar week. Event bars are rendered in week-level lanes layered across the date columns rather than independently inside each cell.
|
||||
|
||||
For each normalized occurrence that intersects the viewed month:
|
||||
|
||||
1. Clip its visible interval to the month-grid range.
|
||||
2. Split the visible interval at week boundaries.
|
||||
3. Convert each weekly segment into a start column and column span.
|
||||
4. Assign segments to the first available lane that does not overlap another segment in that week.
|
||||
5. Render the segment as a linked bar spanning the relevant date columns.
|
||||
|
||||
Each weekly segment repeats the event title. A segment has rounded edges at the occurrence's true visible start or end. An edge is squared and displays a continuation indicator when the occurrence continues before or after that weekly segment. Month-boundary clipping also uses a continuation indicator so the user can see that the event extends beyond the current view.
|
||||
|
||||
Single-day occurrences use the same bar component with a one-column span. Multiple occurrences belonging to the same event are treated independently. Overlapping occurrences occupy separate lanes and never cover one another.
|
||||
|
||||
Day cells grow vertically to accommodate the required event lanes. The current-day highlight remains behind the date cell and does not obscure event bars.
|
||||
|
||||
## Mobile Agenda
|
||||
|
||||
At a maximum width of approximately 640 pixels, matching the plugin's existing responsive breakpoint, hide the month grid and show a chronological agenda for the selected month.
|
||||
|
||||
Each occurrence intersecting the selected month appears once, including occurrences that start in an earlier month or end in a later month. The agenda item displays the complete start/end range, not one item per day. Items sort by occurrence start, then event title for deterministic ordering. Calendar month navigation and the Today action control both desktop and mobile views.
|
||||
|
||||
## Date and Time Presentation
|
||||
|
||||
Formatting uses WordPress localization functions and the configured site timezone.
|
||||
|
||||
- All-day, one-day: one localized date followed by “All day.”
|
||||
- All-day, multi-day: localized start and end dates followed by “All day.”
|
||||
- Timed, same-day with an end: date plus start–end times.
|
||||
- Timed, same-day without an end: date plus start time.
|
||||
- Timed, multi-day: full start date/time through full end date/time.
|
||||
|
||||
Archive and single-event templates render one formatted range per occurrence. They do not expand a multi-day occurrence into one line per day.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Each event bar is a normal keyboard-focusable link.
|
||||
- The accessible label includes the event title and complete occurrence range, even when the visible segment is clipped.
|
||||
- Continuation arrows are decorative and hidden from assistive technology.
|
||||
- The grid retains weekday headings and meaningful date labels.
|
||||
- The mobile agenda uses semantic list markup and headings or time elements as appropriate.
|
||||
- Color is not the only indication that an event continues across a boundary.
|
||||
|
||||
## Admin and Query Behavior
|
||||
|
||||
The Events admin column displays the earliest normalized occurrence and a count of additional occurrences. Admin sorting uses the earliest occurrence start, then falls back to legacy values and post date. Existing REST ordering behavior remains compatible; the occurrence normalization work does not introduce a public recurrence-generation API.
|
||||
|
||||
Calendar data collection continues to include published events. Filtering occurs against occurrence intervals so an event is included when any part of an occurrence intersects the selected month.
|
||||
|
||||
## Theme Compatibility
|
||||
|
||||
The plugin's bundled templates and stylesheet implement the new layout. Theme overrides continue to resolve through the existing loader. A theme-provided `aa-events/calendar.php` remains authoritative and will not automatically acquire week-spanning bars. This limitation must be documented in the release notes or README.
|
||||
|
||||
The normalized occurrence helper is public plugin code so theme overrides can adopt the same data model without duplicating compatibility logic.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Malformed or incomplete repeater rows are skipped safely during rendering and do not break the calendar.
|
||||
- Save-time validation reports actionable row-level errors.
|
||||
- Legacy values that cannot be parsed are left unchanged and are not migrated.
|
||||
- A calendar with no valid occurrences renders its normal empty state.
|
||||
- Duplicate rows are preserved because clients may intentionally create simultaneous occurrences; only automatic legacy migration performs duplicate prevention.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Unit-level tests should cover:
|
||||
|
||||
- Normalization of new, existing-repeater, and legacy field formats.
|
||||
- Idempotent legacy conversion on save.
|
||||
- Optional ends, all-day behavior, and invalid range rejection.
|
||||
- Same-day and multi-day range formatting.
|
||||
- Month intersection and clipping at both boundaries.
|
||||
- Week splitting with configurable WordPress week starts.
|
||||
- Deterministic lane assignment for overlapping segments.
|
||||
- Site-timezone and daylight-saving boundaries.
|
||||
- Admin earliest-occurrence selection and sorting fallbacks.
|
||||
|
||||
Rendering tests should cover:
|
||||
|
||||
- One-column and multi-column bars.
|
||||
- Week and month continuation indicators.
|
||||
- Repeated titles on every weekly segment.
|
||||
- Escaped titles and accessible full-range labels.
|
||||
- Desktop month-grid semantics.
|
||||
- Mobile agenda ordering and one-entry-per-occurrence behavior.
|
||||
|
||||
Manual verification should include the built-in editor's conditional controls, a mixture of recurring and multi-day rows on one event, keyboard navigation, a theme override, and layouts at widths immediately above and below the mobile breakpoint.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Rule-based recurrence generation such as “every Tuesday until September.” Editors continue adding occurrence rows explicitly.
|
||||
- Drag-and-drop calendar editing.
|
||||
- Per-event colors or category-based color configuration.
|
||||
- A bulk migration command for all legacy events.
|
||||
- Changes to calendar month-navigation URLs.
|
||||
@@ -0,0 +1,62 @@
|
||||
# ACF Field-Key Switching Compatibility Design
|
||||
|
||||
## Problem
|
||||
|
||||
The built-in and theme-local field groups use different ACF field keys. Event
|
||||
415 currently has canonical `event_dates` metadata whose hidden reference
|
||||
points to the built-in repeater key, while the active database copy of the
|
||||
theme group still defines its repeater as `multiple_dates`. ACF cannot resolve
|
||||
the inactive key through `get_field()`, so normal field loading can return only
|
||||
the raw repeater count instead of its rows. The front end remains visible
|
||||
because AA Events falls back to the standalone `date` field.
|
||||
|
||||
The updated theme JSON has not yet been synchronized into ACF's database field
|
||||
definitions.
|
||||
|
||||
## Field-Group Synchronization
|
||||
|
||||
Import the existing theme JSON group through ACF's field-group import API. The
|
||||
import retains every stable theme field key while updating the database copy
|
||||
to use the canonical `event_dates`, `Y-m-d`, and `H:i:s` contract.
|
||||
|
||||
The import is a one-time site synchronization, not an automatic plugin
|
||||
mutation of arbitrary external ACF groups.
|
||||
|
||||
## Runtime Field Loading
|
||||
|
||||
AA Events will resolve a supported field through the currently registered ACF
|
||||
field definition when one is available, rather than relying only on the hidden
|
||||
metadata reference. This lets `event_dates` load through whichever field key
|
||||
is active:
|
||||
|
||||
- Built-in: `field_61b0c7f0a3e8f`.
|
||||
- MNBC local group: `field_69fb5b1aac54a`.
|
||||
|
||||
The existing `get_field()` path remains the fallback for tests, legacy fields,
|
||||
and installations where the lower-level ACF APIs are unavailable.
|
||||
|
||||
## Legacy Repeater Migration
|
||||
|
||||
Add an idempotent migration helper for event posts:
|
||||
|
||||
1. Do nothing when canonical `event_dates` contains rows.
|
||||
2. Read and normalize legacy `multiple_dates` rows.
|
||||
3. Write equivalent canonical rows to the currently registered
|
||||
`event_dates` field.
|
||||
4. Preserve the legacy metadata rather than deleting it.
|
||||
|
||||
Run this migration once for the live MNBC events after synchronizing the field
|
||||
group. Keep the helper available on event saves so a legacy-only event becomes
|
||||
canonical when edited.
|
||||
|
||||
Canonical rows remain authoritative if both formats exist.
|
||||
|
||||
## Verification
|
||||
|
||||
- Add regression tests for loading a canonical repeater through an active ACF
|
||||
definition even when its hidden reference belongs to the inactive group.
|
||||
- Add idempotent migration tests for legacy-only and already-canonical events.
|
||||
- Run the complete plugin test suite and PHP syntax checks.
|
||||
- Confirm the live ACF database field definition is named `event_dates`.
|
||||
- Confirm event 415 returns an occurrence from `event_dates`.
|
||||
- Inspect the raw metadata and verify no legacy schedule metadata was deleted.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Local ACF Override Compatibility Design
|
||||
|
||||
## Problem
|
||||
|
||||
AA Events suppresses its built-in ACF field group when the MNBC theme's local
|
||||
field group is enabled. The theme group stores repeating dates under
|
||||
`multiple_dates` and returns dates and times as `F j, Y` and `g:i a`. The
|
||||
plugin's occurrence reader only reads `event_dates` and strictly parses
|
||||
`Y-m-d` dates and 24-hour times. As a result, the plugin detects the local
|
||||
group as an override but cannot turn its schedule values into occurrences, so
|
||||
the events do not appear in schedule-driven views.
|
||||
|
||||
## Scope
|
||||
|
||||
This change makes the plugin compatible with the existing MNBC schedule fields
|
||||
and normalizes the theme's local ACF JSON to the documented AA Events schedule
|
||||
contract. It does not rename or otherwise change unrelated cost, location, or
|
||||
contact fields.
|
||||
|
||||
## Plugin Compatibility
|
||||
|
||||
`aa_events_get_occurrences()` will use schedule sources in this order:
|
||||
|
||||
1. The canonical `event_dates` repeater when it contains rows.
|
||||
2. The legacy local-override `multiple_dates` repeater when it contains rows.
|
||||
3. The existing `event_datetime` fallback.
|
||||
4. The existing standalone `date` and `start_time` fallback.
|
||||
|
||||
Canonical rows take precedence when both repeaters contain data. Legacy
|
||||
`multiple_dates` rows will normalize to the same public occurrence shape as
|
||||
canonical rows, without adding a new public source type.
|
||||
|
||||
The occurrence parsing boundary will accept the plugin's canonical return
|
||||
formats and the formats emitted by the existing theme override:
|
||||
|
||||
- Dates: `Y-m-d`, compact ACF `Ymd`, and `F j, Y`.
|
||||
- Times: `H:i:s`, `H:i`, and `g:i a`.
|
||||
|
||||
All accepted values will be normalized to `Y-m-d` and `H:i:s` before existing
|
||||
range validation, sorting, and rendering logic runs. Invalid or ambiguous
|
||||
values will continue to be rejected rather than guessed.
|
||||
|
||||
Metadata-change synchronization will recognize both repeater names and their
|
||||
schedule subfields. ACF value-cache flushing will include `multiple_dates`, so
|
||||
direct metadata changes cannot leave stale canonical occurrence indexes.
|
||||
|
||||
## Theme ACF JSON
|
||||
|
||||
The MNBC theme field group at
|
||||
`wp-content/themes/MNBC/acf/group_69fb58894f83f.json` will be updated as
|
||||
follows:
|
||||
|
||||
- Rename the repeater field from `multiple_dates` to `event_dates`, retaining
|
||||
its stable ACF field key.
|
||||
- Change the standalone and repeater Date fields' return format to `Y-m-d`.
|
||||
- Change the standalone and repeater Start Time and End Time fields' return
|
||||
format to `H:i:s`.
|
||||
|
||||
The existing field keys remain unchanged. No unrelated field definitions will
|
||||
be modified.
|
||||
|
||||
## Existing Data
|
||||
|
||||
Renaming the ACF field does not rename previously saved WordPress metadata.
|
||||
Existing rows stored as `multiple_dates_*` therefore remain in place and are
|
||||
read through the plugin compatibility fallback. Newly edited schedule data
|
||||
will be saved under canonical `event_dates_*` keys.
|
||||
|
||||
No destructive or automatic bulk database migration is included. If both old
|
||||
and new repeater data exist on an event, canonical `event_dates` data is
|
||||
authoritative.
|
||||
|
||||
## Testing
|
||||
|
||||
Regression tests will first reproduce the current theme override:
|
||||
|
||||
- `multiple_dates` rows using `F j, Y` dates and `g:i a` times produce
|
||||
normalized occurrences.
|
||||
- Standalone legacy fields in those formats produce an occurrence.
|
||||
- Canonical `event_dates` rows win when both repeater names contain data.
|
||||
- Invalid formatted values remain excluded.
|
||||
- Metadata synchronization recognizes and flushes the legacy repeater.
|
||||
|
||||
The focused occurrence tests and complete plugin test suite must pass after
|
||||
the implementation. The theme JSON will also be parsed to confirm it remains
|
||||
valid JSON and contains the canonical schedule names and return formats.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Desktop Calendar View Toggle Design
|
||||
|
||||
## Goal
|
||||
|
||||
Let visitors switch between the traditional month grid and the existing chronological agenda on desktop and tablet. Remember the visitor's selection across month navigation and later visits while keeping phone-sized screens permanently in agenda view.
|
||||
|
||||
## Scope
|
||||
|
||||
This change applies to the plugin's bundled calendar template. It reuses the grid and agenda markup already rendered from the same normalized occurrence data.
|
||||
|
||||
The feature includes:
|
||||
|
||||
- A desktop "Calendar / List" view switcher.
|
||||
- Browser-local preference persistence.
|
||||
- Accessible view state and keyboard operation.
|
||||
- Responsive behavior that preserves the existing mobile agenda.
|
||||
- Automated coverage and README updates.
|
||||
|
||||
The feature does not add a new event query, server-side user preference, URL parameter, WordPress setting, or admin control.
|
||||
|
||||
## User Interface
|
||||
|
||||
The switcher sits in the existing utility row opposite the Today button. It contains two text buttons:
|
||||
|
||||
- Calendar
|
||||
- List
|
||||
|
||||
The active button is visually distinct and exposes `aria-pressed="true"`; the inactive button exposes `aria-pressed="false"`. Both controls have visible keyboard focus styles. The utility row can wrap when space is constrained.
|
||||
|
||||
Above 640 pixels, the selected view is visible and the other view is hidden. At 640 pixels and below, the switcher is hidden, the grid is hidden, and the list is always visible regardless of the saved desktop preference.
|
||||
|
||||
## Rendering and State
|
||||
|
||||
`templates/calendar.php` continues to render both views from `$calendar_occurrences`. The template adds:
|
||||
|
||||
- A switcher associated with its containing calendar instance.
|
||||
- Stable data attributes that identify the Calendar and List controls.
|
||||
- Stable data attributes on the grid and agenda view containers.
|
||||
- Initial accessible button state matching the no-JavaScript desktop default.
|
||||
|
||||
A small dependency-free frontend script finds every bundled calendar instance on the page. It reads one plugin-wide preference from `localStorage`, so a visitor's selection applies consistently to every AA Events calendar.
|
||||
|
||||
The stored values are limited to `calendar` and `list`. A missing or invalid value defaults to `calendar`. Selecting a control updates all instances on the page, adjusts each button's `aria-pressed` state, hides the inactive view with the `hidden` attribute, and stores the new preference.
|
||||
|
||||
Month navigation remains unchanged. Because the preference is browser-local rather than encoded in the URL, existing Previous Month, Next Month, and Today links retain their current URLs and the saved preference is reapplied after navigation.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
CSS remains the authoritative mobile fallback:
|
||||
|
||||
- At 640 pixels and below, the desktop grid and switcher are hidden and the agenda is shown.
|
||||
- Above 640 pixels, JavaScript applies the saved desktop preference.
|
||||
|
||||
The script listens for changes to the 640-pixel media query. Crossing into mobile ensures the agenda is exposed even when Calendar is saved. Crossing back to desktop reapplies the saved selection. Resizing does not overwrite the visitor's preference.
|
||||
|
||||
## Progressive Enhancement and Failure Handling
|
||||
|
||||
Without JavaScript, behavior remains unchanged: desktop and tablet show the month grid, while phone-sized screens show the agenda.
|
||||
|
||||
Access to `localStorage` can fail in restricted browsing contexts. Reads and writes are wrapped in error handling. If reading fails, Calendar is the initial desktop view. The switcher still changes the current page's view when saving fails.
|
||||
|
||||
The frontend script is safe to enqueue on pages without a bundled calendar and exits without side effects when no matching instance exists. A theme-provided `aa-events/calendar.php` remains authoritative and does not automatically gain the switcher unless it adopts the new markup contract.
|
||||
|
||||
## Assets and Compatibility
|
||||
|
||||
Add a dedicated frontend calendar script and enqueue it without external dependencies. Keep `assets/css/aa-events.css` and the bundled `templates/aa-events.css` copy synchronized.
|
||||
|
||||
The new markup and script must support multiple calendar instances without duplicate IDs or state divergence. Existing calendar query, occurrence sorting, grid layout, month navigation, and mobile agenda content are unchanged.
|
||||
|
||||
## Testing
|
||||
|
||||
Automated tests will verify:
|
||||
|
||||
- The rendered switcher contains Calendar and List buttons with correct initial accessible state.
|
||||
- Calendar instances and their two views expose the expected script hooks without duplicate IDs.
|
||||
- CSS places and styles the utility-row switcher, provides selected and focus states, and preserves the 640-pixel forced-list behavior.
|
||||
- The frontend script defaults to Calendar for missing or invalid stored values.
|
||||
- Selecting either control updates every rendered calendar, view visibility, and `aria-pressed` state.
|
||||
- The preference survives script initialization and is not overwritten by the forced mobile layout.
|
||||
- Storage read and write failures do not prevent current-page switching.
|
||||
- Pages without calendar markup are unaffected.
|
||||
|
||||
Run the existing PHP test suite, PHP syntax checks for changed PHP files, and the JavaScript tests introduced for the view controller. Manual verification should cover keyboard use, month navigation, page reload, multiple calendars, resizing immediately above and below 640 pixels, and a browser context where storage is unavailable.
|
||||
|
||||
## Documentation
|
||||
|
||||
Update the README to state that desktop and tablet visitors can switch between Calendar and List, that the selection persists in that browser, and that screens at 640 pixels and below always use List.
|
||||
|
||||
Document that theme template and stylesheet overrides remain authoritative and need to adopt the new markup and styles to receive the switcher.
|
||||
Reference in New Issue
Block a user