Reference
Events

Events

Part of @cobuntu/embed-react: headless, data via props, links through your router.

Event filters

EventFilters is the community app's events browse strip, minus the events: a search box, a row of category pills (and a row of sub-category pills once a category is chosen), a Filters button with When, Sort, Price and Format, Clear, and a result line. EventMiniCalendar is the month grid for jumping to a day. Neither renders event cards: you fetch with events.list and keep your own list.

The state is one plain object:

interface EventFilterState {
  q: string;
  when: "upcoming" | "past";
  category: string;      // "" = all
  subCategory: string;   // "" = all
  sort: "soonest" | "name";
  price: "" | "free" | "paid";
  format: "" | "in_person" | "online" | "hybrid";
  date: string | null;   // "YYYY-MM-DD"
}

Behaviour, as in the app:

  • Choosing a different category clears the sub-category.
  • Picking the selected pill again sets it back to All.
  • Every option shows its count from events.facets. Upcoming and Soonest are the defaults and do not count as filters.
  • Clear resets everything except the search text.

Server-side usage

Read the URL, fetch the page and the counts, render. The helpers are also exported from @cobuntu/embed-react/events, which is not a client module, so a server component can call them.

// app/events/page.tsx (server component)
import { fromSearchParams, toEventQuery, toEventFacetsQuery } from "@cobuntu/embed-react/events";
import { cobuntu } from "@/lib/cobuntu";
import { EventsBrowser } from "./EventsBrowser";
 
export default async function EventsPage({ searchParams }: { searchParams: Promise<Record<string, string>> }) {
  const state = fromSearchParams(await searchParams);
  const [list, facets] = await Promise.all([
    cobuntu.events.list({ ...toEventQuery(state), limit: 24 }),
    cobuntu.events.facets(toEventFacetsQuery(state)),
  ]);
  return <EventsBrowser initial={state} list={list} facets={facets} />;
}

URL sync

toSearchParams(state, base?) writes the state (defaults left out, other keys in base kept); fromSearchParams(params) reads it back from URLSearchParams or a page's searchParams object. The keys are q, when, categoryId, subCategoryId, sort, price, format and date.

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { EventFilters, EventMiniCalendar, toSearchParams, type EventFilterState } from "@cobuntu/embed-react";
 
export function EventsBrowser({ initial, list, facets }) {
  const router = useRouter();
  const [state, setState] = useState<EventFilterState>(initial);
  const update = (next: EventFilterState) => {
    setState(next);
    const qs = toSearchParams(next, window.location.search).toString();
    router.replace(qs ? `?${qs}` : "?", { scroll: false }); // re-runs the server component
  };
  return (
    <>
      <EventFilters value={state} onChange={update} facets={facets} resultCount={list.events.length} />
      <EventMiniCalendar value={state.date} onChange={(date) => update({ ...state, date })} events={list.events} />
      <MyEventList events={list.events} />
    </>
  );
}

Debounce the search text before you navigate or fetch, if you fetch per keystroke. Without value, the component is uncontrolled: pass defaultValue and read changes from onChange.

EventFilters props

Prop
value / defaultValueControlled state, or the starting state when uncontrolled (partial).
onChange(next)Every change, with the sub-category already cleared when the category changed.
facetsThe events.facets() result. Supplies the categories and every count. Without it, no category rows and no counts.
resultCountNumber for the result line and the sheet's Show button. Omit to hide the line.
show{ search?, categories?, when?, sort?, price?, format? }: set any to false to hide it.
trailingA node at the end of the toolbar row.
categoriesIn"inline" (default) or "sheet". With "sheet", the category row (and the sub-category row once a category is chosen) move to the top of the Filters sheet, the toolbar stays search plus one Filters button, and the button's badge and the result line still count the chosen category. The facet is titled labels.category.
labelsAny subset of EventFilterLabels (results: (n) => ..., show: (n) => ...).

EventMiniCalendar props

Prop
value, onChange(day | null)The selected YYYY-MM-DD. Picking it again clears it. toEventQuery sends it as from = to.
eventsAnything with a startDate: draws up to three dots per day, and only days with events can be picked. Omit to make every day pickable.
locale, weekStartsOnMonth and weekday names; 0 Sunday (default) or 1 Monday.
initialMonthYYYY-MM shown first. Defaults to the selected day's month, else today's.

Helpers

Helper
toEventQuery(state)The events.list params.
toEventFacetsQuery(state)The events.facets params (when, search, day).
fromSearchParams(params), toSearchParams(state, base?)URL sync. Unknown values fall back to defaults.
eventFacetsToOptions(facets, category, labels)Facets to { categories, subcategories, price, format, total } option lists, for your own controls.
applyEventFilterChange(prev, next)The sub-category clearing rule.
activeEventFilterCount(state), EMPTY_EVENT_FILTERS, dayToRange(day)

Event price

The price a card or an event page shows, read from the event's tiers. Same rules as Cobuntu's community app:

  • Only paid tiers count. Tiers on sale right now are preferred; when none is on sale, every paid tier is a candidate, so an event between sales windows still shows a price.
  • One candidate shows its price; more than one shows "From" the cheapest.
  • No paid tier is "Free". Every tier sold out is "Sold out".
  • The member price comes from the server (memberPrice, only for a signed-in member who qualifies) and is shown only when it is lower.

The helpers are pure and live in the server-safe @cobuntu/embed-react/events entry as well as the main one.

import { eventPrice, eventPriceTags, instalmentLabel } from "@cobuntu/embed-react/events";
 
const p = eventPrice(event, { locale: "en-IE", labels: { from: (x) => `from ${x}`, free: "free" } });
// { label: "from €15.00", kind: "from", amount: 1500, currency: "EUR", price: "€15.00",
//   memberAmount: 1200, memberPrice: "€12.00", memberLabel: "€12.00 for members" }

Or the headless component:

import { EventPrice } from "@cobuntu/embed-react";
 
<EventPrice event={event} locale="en-IE" labels={{ soldOut: "Sold out" }} />
Helper
eventPrice(event, { locale?, labels? }){ label, kind, amount, currency, price, memberAmount?, memberPrice?, memberLabel? }. kind is free, single, from or soldOut.
eventPriceTags(event, { locale? })One tag per candidate paid tier, cheapest first, duplicates collapsed: { list, member, amount, memberAmount, currency }. Empty for a free event.
isEventFree(event), isEventSoldOut(event)Ask the event, not the label.
isTierSoldOut(tier), tierRemaining(tier)Per tier. tierRemaining is null when uncapped or unknown.
instalmentPlan(tier), instalmentLabel(tier, { locale?, format? })The tier's instalment plan, and "or €20.33 every month × 3 charges". Null without a plan.
defaultEventPriceLabelsfree, from(price), soldOut, member(price).

EventPrice props: event, locale, labels, showMember (default true), className. Classes: cb-event-price (data-kind), __label, __member.

Class map

See the class map conventions.

Event filters

ClassElementState attributes
cb-event-filtersRootdata-active, data-when
__rowToolbar row
__search / __search-input / __search-clearSearchdata-filled
__toggle / __toggle-label / __badgeFilters button and active countdata-active, aria-expanded
__clearClear (only while something is applied)
__pillsA pill rowdata-facet="category | subCategory", data-depth="0 | 1"
__pill / __pill-label / __pill-countA pill (the first is All)data-state="on | off", data-value, data-empty when its count is 0
__summary / __countResult line
__backdrop, __sheet, __sheet-header, __sheet-title, __close, __body, __footer, __reset, __doneThe sheet
__facet / __facet-title / __optionsA group in the sheetdata-facet="when | sort | price | format"
__option / __option-label / __option-countAn optionas __pill

Mini calendar

ClassElementState attributes
cb-mini-calRootdata-has-selection
__header, __month, __nav, __prev, __today, __nextMonth and navigation
__weekdays / __weekdayWeekday initials
__grid / __dayDays (a button when pickable)data-date, data-state, data-today, data-count, data-empty, data-outside
__dots / __dotUp to three event dots
__selection, __selected, __clearThe chosen day and its clear button