{"id":313469,"date":"2026-08-17T09:38:50","date_gmt":"2026-08-17T09:38:50","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/eventspress\/"},"modified":"2026-10-25T14:46:07","modified_gmt":"2026-10-25T14:46:07","slug":"happenboard","status":"publish","type":"plugin","link":"https:\/\/pe.wordpress.org\/plugins\/happenboard\/","author":12819032,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.1","stable_tag":"1.1","tested":"7.1.3","requires":"6.9","requires_php":"8.2","requires_plugins":null,"header_name":"HappenBoard","header_author":"rainbowgeek","header_description":"Block-native events calendar \u2014 recurring events, RSVPs, iCal feeds, and Event Schema markup. Built for the Site Editor.","assets_banners_color":"f7f0c6","last_updated":"2026-10-25 14:46:07","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/profiles.wordpress.org\/rainbowgeek\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":438,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0":{"tag":"1.0","author":"rainbowgeek","date":"2026-10-25 14:46:07","revision":3764775},"1.1":{"tag":"1.1","author":"rainbowgeek","date":"2026-10-25 14:46:07","revision":3764775}},"upgrade_notice":{"1.1.0":"<p>Events, venues and organizers get their own permissions: Authors and Contributors lose access unless a role editor gives it back.<\/p>","1.0.0":"<p>First stable release. Database update: recurring events are rebuilt in your site&#039;s time zone and RSVPs get their date. Back up first. Required by HappenBoard Pro 1.0.0.<\/p>"},"ratings":[],"assets_icons":{"icon.svg":{"filename":"icon.svg","revision":3715552,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3715552,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3715552,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3764778,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"meta\":{\"title\":\"HappenBoard demo\",\"description\":\"A community centre site with events, recurring classes, RSVPs, booking types and bookings.\",\"author\":\"HappenBoard\",\"categories\":[\"Events\"]},\"landingPage\":\"\\\/wp-admin\\\/admin.php?page=happenboard%2Fadmin.php\",\"preferredVersions\":{\"php\":\"8.5\",\"wp\":\"latest\"},\"features\":{\"networking\":true},\"steps\":[{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"happenboard\"},\"options\":{\"activate\":true}},{\"step\":\"runPHP\",\"code\":\"<?php\\n\\\/**\\n * Demo content for the WordPress Playground preview.\\n *\\n * `bin\\\/build-blueprint.mjs` inlines this file into blueprint.json as a\\n * `runPHP` step, so the wordpress.org \\\"Live Preview\\\" (and the iframe on\\n * happenboard.com) opens on a lived-in site instead of an empty one: a\\n * fictional community centre in Portland with venues, organizers, one-off,\\n * recurring and multi-day events, RSVPs (one event sold out with a\\n * waitlist), booking types with weekly availability, and bookings.\\n *\\n * Every date is computed from the moment the preview starts, so the\\n * calendar always has events this week, next week and last week.\\n *\\n * Photos come from Unsplash (free Unsplash License). A photo that fails to\\n * download is skipped: the event is still created, just without an image.\\n *\\n * It writes through the plugin's own tables and services, the way\\n * `wp happenboard seed` (src\\\/Dev\\\/SeedCommand.php) does, so keep the two in\\n * step when a meta key or a column changes.\\n *\\n * @package HappenBoard\\n *\\\/\\n\\nrequire_once '\\\/wordpress\\\/wp-load.php';\\nrequire_once ABSPATH . 'wp-admin\\\/includes\\\/file.php';\\nrequire_once ABSPATH . 'wp-admin\\\/includes\\\/media.php';\\nrequire_once ABSPATH . 'wp-admin\\\/includes\\\/image.php';\\n\\nif ( ! class_exists( \\\\HappenBoard\\\\Service\\\\Events\\\\OccurrenceSyncer::class ) ) {\\n\\treturn;\\n}\\n\\nwp_set_current_user( 1 );\\n\\n\\\/\\\/ No emails leave a preview.\\nadd_filter( 'pre_wp_mail', '__return_false' );\\n\\n$hb_tz  = new DateTimeZone( 'America\\\/Los_Angeles' );\\n$hb_utc = new DateTimeZone( 'UTC' );\\n\\nupdate_option( 'blogname', 'Riverside Community Hub' );\\nupdate_option( 'blogdescription', 'Classes, concerts and meetups in Portland' );\\nupdate_option( 'timezone_string', 'America\\\/Los_Angeles' );\\nupdate_option( 'date_format', 'F j, Y' );\\nupdate_option( 'time_format', 'g:i a' );\\nupdate_option( 'permalink_structure', '\\\/%postname%\\\/' );\\n\\n\\\/\\\/ The admin opens on the dashboard, not on the setup wizard.\\nupdate_option( 'happenboard_onboarded', 1, false );\\n$hb_settings                    = (array) get_option( 'happenboard_general_settings', array() );\\n$hb_settings['weather_enabled'] = true;\\n$hb_settings['currency']        = 'USD';\\nupdate_option( 'happenboard_general_settings', $hb_settings );\\n\\nwp_update_user(\\n\\tarray(\\n\\t\\t'ID'           => 1,\\n\\t\\t'first_name'   => 'Alex',\\n\\t\\t'last_name'    => 'Rivera',\\n\\t\\t'display_name' => 'Alex Rivera',\\n\\t)\\n);\\n\\n\\\/\\\/ Core's \\\"Welcome to the editor\\\" tour would cover the first event opened.\\nupdate_user_meta(\\n\\t1,\\n\\t$wpdb->get_blog_prefix() . 'persisted_preferences',\\n\\tarray(\\n\\t\\t'core\\\/edit-post' => array( 'welcomeGuide' => false ),\\n\\t\\t'core\\\/edit-site' => array( 'welcomeGuide' => false ),\\n\\t\\t'_modified'      => gmdate( 'c' ),\\n\\t)\\n);\\n\\nmt_srand( 20261020 );\\n\\n$hb_now   = new DateTimeImmutable( 'now', $hb_tz );\\n$hb_today = $hb_now->setTime( 0, 0 );\\n\\n\\\/**\\n * A local date, $days from today, at $time (\\\"H:i\\\").\\n *\\\/\\n$hb_at = static function ( int $days, string $time ) use ( $hb_today ): DateTimeImmutable {\\n\\t[ $h, $m ] = array_map( 'intval', explode( ':', $time ) );\\n\\treturn $hb_today->modify( ( $days >= 0 ? '+' : '' ) . $days . ' days' )->setTime( $h, $m );\\n};\\n\\n\\\/**\\n * The first $weekday (1 = Monday \\u2026 7 = Sunday) on or after $from.\\n *\\\/\\n$hb_next_weekday = static function ( DateTimeImmutable $from, int $weekday ): DateTimeImmutable {\\n\\t$shift = ( $weekday - (int) $from->format( 'N' ) + 7 ) % 7;\\n\\treturn $from->modify( '+' . $shift . ' days' );\\n};\\n\\n$hb_iso = static fn( DateTimeImmutable $d ): string => $d->setTimezone( $hb_utc )->format( 'Y-m-d\\\\TH:i:s\\\\Z' );\\n$hb_sql = static fn( DateTimeImmutable $d ): string => $d->setTimezone( $hb_utc )->format( 'Y-m-d H:i:s' );\\n\\n\\\/**\\n * Downloads an Unsplash photo into the media library. 0 when it fails.\\n *\\\/\\n$hb_photo = static function ( string $photo, string $name, string $alt ): int {\\n\\t$tmp = download_url( 'https:\\\/\\\/images.unsplash.com\\\/photo-' . $photo . '?w=1600&h=1000&fit=crop&fm=jpg&q=78', 30 );\\n\\tif ( is_wp_error( $tmp ) ) {\\n\\t\\treturn 0;\\n\\t}\\n\\t$hb_id = media_handle_sideload(\\n\\t\\tarray(\\n\\t\\t\\t'name'     => $name . '.jpg',\\n\\t\\t\\t'tmp_name' => $tmp,\\n\\t\\t),\\n\\t\\t0\\n\\t);\\n\\tif ( is_wp_error( $hb_id ) ) {\\n\\t\\twp_delete_file( $tmp );\\n\\t\\treturn 0;\\n\\t}\\n\\tupdate_post_meta( $hb_id, '_wp_attachment_image_alt', $alt );\\n\\treturn (int) $hb_id;\\n};\\n\\n\\\/\\\/ Generating every intermediate size is slow in WebAssembly; three cover\\n\\\/\\\/ what the theme and the blocks show.\\nadd_filter(\\n\\t'intermediate_image_sizes_advanced',\\n\\tstatic fn( array $sizes ): array => array_intersect_key( $sizes, array_flip( array( 'thumbnail', 'medium', 'medium_large', 'large' ) ) )\\n);\\nadd_filter( 'big_image_size_threshold', '__return_false' );\\n\\n\\\/* ------------------------------------------------------------- people *\\\/\\n\\n$hb_host = static function ( string $login, string $first, string $last ): int {\\n\\t$user = get_user_by( 'login', $login );\\n\\tif ( $user ) {\\n\\t\\t$hb_id = (int) $user->ID;\\n\\t} else {\\n\\t\\t$hb_id = wp_insert_user(\\n\\t\\t\\tarray(\\n\\t\\t\\t\\t'user_login'   => $login,\\n\\t\\t\\t\\t'user_email'   => $login . '@example.com',\\n\\t\\t\\t\\t'user_pass'    => wp_generate_password( 24 ),\\n\\t\\t\\t\\t'first_name'   => $first,\\n\\t\\t\\t\\t'last_name'    => $last,\\n\\t\\t\\t\\t'display_name' => $first . ' ' . $last,\\n\\t\\t\\t\\t'role'         => 'editor',\\n\\t\\t\\t)\\n\\t\\t);\\n\\t\\tif ( is_wp_error( $hb_id ) ) {\\n\\t\\t\\treturn 1;\\n\\t\\t}\\n\\t}\\n\\t( new WP_User( $hb_id ) )->add_cap( 'happenboard_host' );\\n\\treturn (int) $hb_id;\\n};\\n\\n$hb_maya   = $hb_host( 'maya', 'Maya', 'Chen' );\\n$hb_daniel = $hb_host( 'daniel', 'Daniel', 'Brooks' );\\n( new WP_User( 1 ) )->add_cap( 'happenboard_host' );\\n\\n$hb_first  = array( 'Olivia', 'Liam', 'Emma', 'Noah', 'Ava', 'Elijah', 'Sophia', 'Mateo', 'Isabella', 'Lucas', 'Mia', 'Ethan', 'Amelia', 'James', 'Harper', 'Benjamin', 'Evelyn', 'Leo', 'Aria', 'Henry', 'Chloe', 'Samuel', 'Zoe', 'Jack', 'Priya', 'Omar', 'Hana', 'Diego', 'Grace', 'Kenji', 'Nora', 'Ravi', 'Lily', 'Malik', 'Elena', 'Theo' );\\n$hb_last   = array( 'Smith', 'Johnson', 'Nguyen', 'Garcia', 'Patel', 'Kim', 'Brown', 'Martinez', 'Davis', 'Lopez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Lee', 'Walker', 'Hall', 'Young', 'Allen', 'Wright', 'Scott', 'Torres', 'Hill', 'Green', 'Adams', 'Baker', 'Rivera', 'Campbell', 'Mitchell', 'Ortiz', 'Okafor', 'Silva', 'Novak', 'Sato' );\\n$hb_person = static function ( int $n ) use ( $hb_first, $hb_last ): array {\\n\\t\\\/\\\/ 36 \\u00d7 36 distinct pairs: no two people share a name below 1,296.\\n\\t$first = $hb_first[ $n % 36 ];\\n\\t$last  = $hb_last[ ( $n * 7 + intdiv( $n, 36 ) ) % 36 ];\\n\\treturn array( $first . ' ' . $last, strtolower( $first . '.' . $last ) . '@example.com' );\\n};\\n\\n\\\/* --------------------------------------------------------- taxonomies *\\\/\\n\\n$hb_terms = static function ( string $taxonomy, array $defs ): array {\\n\\t$out = array();\\n\\tforeach ( $defs as $slug => $def ) {\\n\\t\\t$term = get_term_by( 'slug', $slug, $taxonomy );\\n\\t\\t$hb_id   = $term ? (int) $term->term_id : 0;\\n\\t\\tif ( ! $hb_id ) {\\n\\t\\t\\t$res = wp_insert_term( $def[0], $taxonomy, array( 'slug' => $slug ) );\\n\\t\\t\\tif ( is_wp_error( $res ) ) {\\n\\t\\t\\t\\tcontinue;\\n\\t\\t\\t}\\n\\t\\t\\t$hb_id = (int) $res['term_id'];\\n\\t\\t}\\n\\t\\tif ( ! empty( $def[1] ) ) {\\n\\t\\t\\tupdate_term_meta( $hb_id, 'happenboard_color', $def[1] );\\n\\t\\t}\\n\\t\\t$out[ $slug ] = $hb_id;\\n\\t}\\n\\treturn $out;\\n};\\n\\n$hb_cats = $hb_terms(\\n\\t'happenboard_category',\\n\\tarray(\\n\\t\\t'music'     => array( 'Music', '#7c3aed' ),\\n\\t\\t'workshops' => array( 'Workshops', '#ea580c' ),\\n\\t\\t'wellness'  => array( 'Wellness', '#059669' ),\\n\\t\\t'tech'      => array( 'Tech', '#2563eb' ),\\n\\t\\t'family'    => array( 'Family', '#d97706' ),\\n\\t\\t'community' => array( 'Community', '#db2777' ),\\n\\t)\\n);\\n$hb_tags = $hb_terms(\\n\\t'happenboard_tag',\\n\\tarray(\\n\\t\\t'free'              => array( 'Free' ),\\n\\t\\t'beginner-friendly' => array( 'Beginner-friendly' ),\\n\\t\\t'outdoors'          => array( 'Outdoors' ),\\n\\t\\t'online'            => array( 'Online' ),\\n\\t\\t'kids-welcome'      => array( 'Kids welcome' ),\\n\\t\\t'evening'           => array( 'Evening' ),\\n\\t\\t'registration-only' => array( 'Registration required' ),\\n\\t)\\n);\\n\\n\\\/* ------------------------------------------------- venues + organizers *\\\/\\n\\n$hb_post = static function ( string $type, string $title, string $excerpt, array $meta, string $content = '' ): int {\\n\\t$hb_id = wp_insert_post(\\n\\t\\tarray(\\n\\t\\t\\t'post_type'    => $type,\\n\\t\\t\\t'post_status'  => 'publish',\\n\\t\\t\\t'post_title'   => $title,\\n\\t\\t\\t'post_excerpt' => $excerpt,\\n\\t\\t\\t'post_content' => $content,\\n\\t\\t\\t'post_author'  => 1,\\n\\t\\t),\\n\\t\\ttrue\\n\\t);\\n\\tif ( is_wp_error( $hb_id ) ) {\\n\\t\\treturn 0;\\n\\t}\\n\\tforeach ( $meta as $key => $value ) {\\n\\t\\tupdate_post_meta( $hb_id, $key, $value );\\n\\t}\\n\\treturn (int) $hb_id;\\n};\\n\\n$hb_paragraphs = static function ( array $paragraphs ): string {\\n\\t$html = '';\\n\\tforeach ( $paragraphs as $p ) {\\n\\t\\t$html .= \\\"<!-- wp:paragraph -->\\\\n<p>\\\" . $p . \\\"<\\\/p>\\\\n<!-- \\\/wp:paragraph -->\\\\n\\\\n\\\";\\n\\t}\\n\\treturn trim( $html );\\n};\\n\\n$hb_venue_meta = static fn( string $address, string $zip, float $lat, float $lng, int $capacity, string $phone ): array => array(\\n\\t'_happenboard_address'  => $address,\\n\\t'_happenboard_city'     => 'Portland',\\n\\t'_happenboard_state'    => 'OR',\\n\\t'_happenboard_zip'      => $zip,\\n\\t'_happenboard_country'  => 'United States',\\n\\t'_happenboard_lat'      => $lat,\\n\\t'_happenboard_lng'      => $lng,\\n\\t'_happenboard_capacity' => $capacity,\\n\\t'_happenboard_phone'    => $phone,\\n);\\n\\n$hb_venues = array(\\n\\t'hall'    => $hb_post( 'hboard_venue', 'Riverside Hall', 'Our main hall by the river: a stage, 300 seats and a bar.', $hb_venue_meta( '1200 SE Water Ave', '97214', 45.5135, -122.6652, 300, '(503) 555-0142' ) ),\\n\\t'loft'    => $hb_post( 'hboard_venue', 'The Loft Studio', 'A bright upstairs studio for small classes and workshops.', $hb_venue_meta( '2025 NW Lovejoy St', '97209', 45.5297, -122.6936, 24, '(503) 555-0178' ) ),\\n\\t'park'    => $hb_post( 'hboard_venue', 'Laurelhurst Park', 'Meet by the pond, near the SE Ankeny St entrance.', $hb_venue_meta( 'SE Cesar E Chavez Blvd & SE Stark St', '97214', 45.5215, -122.6253, 0, '' ) ),\\n\\t'library' => $hb_post( 'hboard_venue', 'Northside Library, Room 2', 'Ground floor community room, step-free access.', $hb_venue_meta( '512 N Killingsworth St', '97217', 45.5626, -122.6747, 40, '(503) 555-0190' ) ),\\n);\\n\\n$hb_org = static fn( string $name, string $about, string $email, string $phone ) => $hb_post(\\n\\t'hboard_organizer',\\n\\t$name,\\n\\t$about,\\n\\tarray(\\n\\t\\t'_happenboard_email' => $email,\\n\\t\\t'_happenboard_phone' => $phone,\\n\\t)\\n);\\n\\n$hb_orgs = array(\\n\\t'arts'     => $hb_org( 'Riverside Arts Collective', 'Volunteers who run the hall\\u2019s concerts, open mics and film nights.', 'hello@riverside-arts.example.com', '(503) 555-0101' ),\\n\\t'yoga'     => $hb_org( 'Sunrise Yoga Studio', 'Gentle, all-levels yoga, indoors in winter and in the park when it\\u2019s dry.', 'namaste@sunrise-yoga.example.com', '(503) 555-0123' ),\\n\\t'code'     => $hb_org( 'Code & Coffee PDX', 'A friendly meetup for people who build things for the web.', 'team@codeandcoffee.example.com', '' ),\\n\\t'explorer' => $hb_org( 'Little Explorers Club', 'Hands-on science and nature activities for kids aged 6 to 12.', 'explore@little-explorers.example.com', '(503) 555-0165' ),\\n);\\n\\n\\\/* ------------------------------------------------------------- events *\\\/\\n\\n\\\/\\\/ Every date sits on a sensible weekday, whatever day the preview starts.\\n$hb_on = static fn( int $days, int $weekday, string $time ): DateTimeImmutable => $hb_next_weekday( $hb_at( $days, $time ), $weekday );\\n\\n$hb_saturday = $hb_on( 1, 6, '00:00' );\\n$hb_friday   = $hb_on( 14, 5, '00:00' );\\n\\n\\\/\\\/ First Thursday of last month, for a series that started before today.\\n$hb_first_thursday = $hb_next_weekday( $hb_today->modify( 'first day of last month' ), 4 );\\n\\n$hb_events = array(\\n\\tarray(\\n\\t\\t'title'    => 'Saturday Morning Yoga in the Park',\\n\\t\\t'excerpt'  => 'An hour of gentle flow under the trees. All levels, bring a mat and a bottle of water.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Start the weekend slowly. Our teachers guide an hour of gentle vinyasa flow on the grass by the pond, with options for every level.',\\n\\t\\t\\t'Bring a mat, a towel and a bottle of water. If it rains, we cancel the class by 7 am and let everyone who registered know by email.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_saturday->modify( '-21 days' )->setTime( 9, 0 ),\\n\\t\\t'minutes'  => 60,\\n\\t\\t'rrule'    => 'FREQ=WEEKLY;BYDAY=SA;COUNT=16',\\n\\t\\t'cancel'   => 2, \\\/\\\/ The third Saturday from now is cancelled.\\n\\t\\t'venue'    => 'park',\\n\\t\\t'org'      => 'yoga',\\n\\t\\t'capacity' => 30,\\n\\t\\t'cats'     => array( 'wellness' ),\\n\\t\\t'tags'     => array( 'free', 'outdoors', 'beginner-friendly' ),\\n\\t\\t'photo'    => array( '1758274526045-2bd03dcc64d2', 'yoga-in-the-park', 'People doing yoga on mats in a park' ),\\n\\t\\t'fill'     => 0.8,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Paint & Sip Evening',\\n\\t\\t'excerpt'  => 'Paint your own landscape, step by step, with a glass of local wine.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'No experience needed: our artist walks you through one painting, brushstroke by brushstroke. You leave with your canvas and, usually, a new hobby.',\\n\\t\\t\\t'Canvas, paints, aprons and one glass of wine or juice are included. 21+ for wine.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_on( 2, 5, '18:30' ),\\n\\t\\t'minutes'  => 150,\\n\\t\\t'venue'    => 'loft',\\n\\t\\t'org'      => 'arts',\\n\\t\\t'capacity' => 20,\\n\\t\\t'cats'     => array( 'workshops' ),\\n\\t\\t'tags'     => array( 'evening', 'registration-only' ),\\n\\t\\t'photo'    => array( '1785439720398-f687f65df767', 'paint-and-sip', 'Easels and canvases set up for a painting class' ),\\n\\t\\t'fill'     => 1.0,\\n\\t\\t'waitlist' => 5,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Getting Started with the Block Editor',\\n\\t\\t'excerpt'  => 'A free one-hour webinar: build a page with blocks, patterns and styles, then ask your questions live.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Building your first site? In one hour we build a complete page together with blocks, patterns and global styles, then keep 15 minutes for your questions.',\\n\\t\\t\\t'The link to join appears on this page and in your confirmation email once you register.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_on( 3, 2, '12:00' ),\\n\\t\\t'minutes'  => 60,\\n\\t\\t'format'   => 'virtual',\\n\\t\\t'provider' => 'zoom',\\n\\t\\t'url'      => 'https:\\\/\\\/zoom.us\\\/j\\\/0000000000',\\n\\t\\t'org'      => 'code',\\n\\t\\t'cats'     => array( 'tech' ),\\n\\t\\t'tags'     => array( 'free', 'online', 'beginner-friendly' ),\\n\\t\\t'photo'    => array( '1588196749597-9ff075ee6b5b', 'block-editor-webinar', 'A laptop showing a video call with many participants' ),\\n\\t\\t'fill'     => 0.5,\\n\\t\\t'people'   => 64,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Jazz Night: The Blue Hour Quartet',\\n\\t\\t'excerpt'  => 'Two sets of warm, swinging jazz from Portland\\u2019s favorite quartet. Doors at 7:30 pm.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'The Blue Hour Quartet is back at Riverside Hall with two sets of standards and originals, from Ellington to Esperanza Spalding.',\\n\\t\\t\\t'Doors open at 7:30 pm, music starts at 8 pm. The bar serves local beer, wine and alcohol-free cocktails.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_on( 6, 6, '20:00' ),\\n\\t\\t'minutes'  => 180,\\n\\t\\t'venue'    => 'hall',\\n\\t\\t'org'      => 'arts',\\n\\t\\t'capacity' => 300,\\n\\t\\t'cats'     => array( 'music' ),\\n\\t\\t'tags'     => array( 'evening' ),\\n\\t\\t'photo'    => array( '1415201364774-f6f0bb35f28f', 'jazz-night', 'A saxophone player on stage under warm lights' ),\\n\\t\\t'fill'     => 0.6,\\n\\t\\t'people'   => 160,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Golden Hour Photo Walk',\\n\\t\\t'excerpt'  => 'Learn to read the light on a relaxed walk around the park. Any camera welcome, phones too.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'A two-hour walk at the best time of day for photos. Our guide shares simple tips on light, framing and composition along the way.',\\n\\t\\t\\t'Any camera works, including your phone. We finish by sharing our favorite shots over hot chocolate.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_on( 8, 7, '16:00' ),\\n\\t\\t'minutes'  => 120,\\n\\t\\t'venue'    => 'park',\\n\\t\\t'org'      => 'arts',\\n\\t\\t'capacity' => 15,\\n\\t\\t'cats'     => array( 'workshops' ),\\n\\t\\t'tags'     => array( 'outdoors', 'beginner-friendly' ),\\n\\t\\t'photo'    => array( '1486684630165-abf69eea2cab', 'photo-walk', 'A person walking through tall grass with a camera' ),\\n\\t\\t'fill'     => 0.6,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Kids\\u2019 Science Saturday',\\n\\t\\t'excerpt'  => 'Slime, volcanoes and dry-ice clouds: a morning of safe, messy experiments for ages 6 to 12.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Little scientists put on a lab coat and goggles and run real experiments: homemade slime, baking-soda volcanoes and clouds of dry ice.',\\n\\t\\t\\t'For kids aged 6 to 12. One adult stays with each group of children. Everything is provided.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_saturday->modify( '+7 days' )->setTime( 10, 30 ),\\n\\t\\t'minutes'  => 90,\\n\\t\\t'venue'    => 'library',\\n\\t\\t'org'      => 'explorer',\\n\\t\\t'capacity' => 25,\\n\\t\\t'cats'     => array( 'family' ),\\n\\t\\t'tags'     => array( 'free', 'kids-welcome', 'registration-only' ),\\n\\t\\t'photo'    => array( '1633828763399-e29f1cd3f4c1', 'kids-science', 'A child in a lab coat and goggles doing an experiment' ),\\n\\t\\t'fill'     => 0.75,\\n\\t),\\n\\tarray(\\n\\t\\t'title'   => 'Riverside Food & Makers Festival',\\n\\t\\t'excerpt' => 'Three days of local food, craft stalls, live music and kids\\u2019 workshops in the park.',\\n\\t\\t'content' => array(\\n\\t\\t\\t'Our biggest weekend of the year. More than 40 local farms, food trucks and makers, a workshop tent for kids and live music every afternoon.',\\n\\t\\t\\t'Free entry, no registration. Food and drinks are sold by the stalls.',\\n\\t\\t),\\n\\t\\t'start'   => $hb_friday->setTime( 0, 0 ),\\n\\t\\t'end'     => $hb_friday->modify( '+2 days' )->setTime( 23, 59, 59 ),\\n\\t\\t'all_day' => true,\\n\\t\\t'venue'   => 'park',\\n\\t\\t'org'     => 'arts',\\n\\t\\t'cats'    => array( 'community', 'family' ),\\n\\t\\t'tags'    => array( 'free', 'outdoors', 'kids-welcome' ),\\n\\t\\t'photo'   => array( '1757609909192-0246239198c0', 'food-and-makers-festival', 'People shopping for fresh produce at an outdoor market' ),\\n\\t\\t'fill'    => 0,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Pottery for Beginners (6-week course)',\\n\\t\\t'excerpt'  => 'Six Wednesday evenings at the wheel: centering, pulling, trimming and glazing your own pieces.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Over six weeks you learn the whole process, from centering clay on the wheel to glazing a set of bowls you take home.',\\n\\t\\t\\t'Clay, tools, glazes and firing are included. Wear clothes that can get muddy.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_next_weekday( $hb_today->modify( '-7 days' ), 3 )->setTime( 19, 0 ),\\n\\t\\t'minutes'  => 120,\\n\\t\\t'rrule'    => 'FREQ=WEEKLY;BYDAY=WE;COUNT=6',\\n\\t\\t'venue'    => 'loft',\\n\\t\\t'org'      => 'arts',\\n\\t\\t'capacity' => 10,\\n\\t\\t'cats'     => array( 'workshops' ),\\n\\t\\t'tags'     => array( 'beginner-friendly', 'evening', 'registration-only' ),\\n\\t\\t'photo'    => array( '1595351298020-038700609878', 'pottery-course', 'Hands shaping clay on a pottery wheel' ),\\n\\t\\t'fill'     => 0.9,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Code & Coffee: Monthly Developer Meetup',\\n\\t\\t'excerpt'  => 'Two short talks, demos and good coffee on the first Thursday of every month. Join in person or online.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Every first Thursday, web developers, designers and curious minds meet for two 15-minute talks, a few demos and plenty of coffee.',\\n\\t\\t\\t'Can\\u2019t make it in person? The talks are streamed live on Google Meet.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_first_thursday->setTime( 18, 30 ),\\n\\t\\t'minutes'  => 120,\\n\\t\\t'rrule'    => 'FREQ=MONTHLY;BYDAY=1TH;COUNT=8',\\n\\t\\t'format'   => 'hybrid',\\n\\t\\t'provider' => 'meet',\\n\\t\\t'url'      => 'https:\\\/\\\/meet.google.com\\\/abc-defg-hij',\\n\\t\\t'venue'    => 'library',\\n\\t\\t'org'      => 'code',\\n\\t\\t'capacity' => 40,\\n\\t\\t'cats'     => array( 'tech' ),\\n\\t\\t'tags'     => array( 'free', 'evening', 'online' ),\\n\\t\\t'photo'    => array( '1522071820081-009f0129c71c', 'code-and-coffee', 'People working on laptops around a wooden table' ),\\n\\t\\t'fill'     => 0.7,\\n\\t),\\n\\tarray(\\n\\t\\t'title'   => 'Open Mic Night',\\n\\t\\t'excerpt' => 'Songs, poems, comedy: five minutes on stage for anyone brave enough. Last Friday of the month.',\\n\\t\\t'content' => array(\\n\\t\\t\\t'Sign up at the door from 7 pm for a five-minute slot, or just come to listen. A piano, two guitars and a sound engineer are ready for you.',\\n\\t\\t),\\n\\t\\t'start'   => $hb_next_weekday( $hb_today->modify( 'last day of last month' )->modify( '-6 days' ), 5 )->setTime( 19, 30 ),\\n\\t\\t'minutes' => 150,\\n\\t\\t'rrule'   => 'FREQ=MONTHLY;BYDAY=-1FR;COUNT=6',\\n\\t\\t'venue'   => 'hall',\\n\\t\\t'org'     => 'arts',\\n\\t\\t'cats'    => array( 'music', 'community' ),\\n\\t\\t'tags'    => array( 'free', 'evening' ),\\n\\t\\t'photo'   => array( '1511671782779-c97d3d27a1d4', 'open-mic', 'A vintage microphone in front of colorful stage lights' ),\\n\\t\\t'fill'    => 0,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Startup Pitch Night',\\n\\t\\t'excerpt'  => 'Eight local founders, five minutes each, one audience vote. Networking drinks afterwards.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Eight early-stage founders from Portland pitch their idea in five minutes, followed by three minutes of questions from our panel.',\\n\\t\\t\\t'The audience votes for its favorite. Stay for drinks and meet the founders afterwards.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_on( 35, 2, '18:00' ),\\n\\t\\t'minutes'  => 180,\\n\\t\\t'format'   => 'hybrid',\\n\\t\\t'provider' => 'zoom',\\n\\t\\t'url'      => 'https:\\\/\\\/zoom.us\\\/j\\\/0000000001',\\n\\t\\t'venue'    => 'hall',\\n\\t\\t'org'      => 'code',\\n\\t\\t'capacity' => 150,\\n\\t\\t'cats'     => array( 'tech', 'community' ),\\n\\t\\t'tags'     => array( 'evening', 'registration-only' ),\\n\\t\\t'photo'    => array( '1505373877841-8d25f7d46678', 'pitch-night', 'A speaker presenting on a big screen to an audience' ),\\n\\t\\t'fill'     => 0.2,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Outdoor Movie Night',\\n\\t\\t'excerpt'  => 'A classic film on the big screen under the stars. Bring a blanket, we bring the popcorn.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'A family-friendly classic on a giant inflatable screen by the pond. Popcorn and lemonade are on us.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_on( -9, 6, '19:00' ),\\n\\t\\t'minutes'  => 150,\\n\\t\\t'venue'    => 'park',\\n\\t\\t'org'      => 'arts',\\n\\t\\t'capacity' => 120,\\n\\t\\t'cats'     => array( 'community', 'family' ),\\n\\t\\t'tags'     => array( 'free', 'outdoors', 'kids-welcome', 'evening' ),\\n\\t\\t'photo'    => array( '1773142349642-25a4564d33f7', 'movie-night', 'People relaxing on cushions at an outdoor film screening' ),\\n\\t\\t'fill'     => 0.7,\\n\\t\\t'people'   => 60,\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Community Park Clean-up',\\n\\t\\t'excerpt'  => 'Gloves, bags and coffee provided: three hours to make the park shine.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Thank you to everyone who came! Together we collected 46 bags of litter and planted 30 native shrubs along the pond.',\\n\\t\\t),\\n\\t\\t'start'    => $hb_on( -16, 7, '10:00' ),\\n\\t\\t'minutes'  => 180,\\n\\t\\t'venue'    => 'park',\\n\\t\\t'org'      => 'arts',\\n\\t\\t'capacity' => 50,\\n\\t\\t'cats'     => array( 'community' ),\\n\\t\\t'tags'     => array( 'free', 'outdoors', 'kids-welcome' ),\\n\\t\\t'photo'    => array( '1758599669406-d5179ccefcb9', 'park-clean-up', 'Smiling volunteers holding bags of litter in a park' ),\\n\\t\\t'fill'     => 0.85,\\n\\t),\\n\\tarray(\\n\\t\\t'title'   => 'Spring Garden Tour',\\n\\t\\t'excerpt' => 'A guided walk through five private gardens in the neighborhood.',\\n\\t\\t'content' => array( 'Draft: waiting for the last two gardens to confirm.' ),\\n\\t\\t'start'   => $hb_at( 160, '10:00' ),\\n\\t\\t'minutes' => 180,\\n\\t\\t'status'  => 'draft',\\n\\t\\t'org'     => 'arts',\\n\\t\\t'cats'    => array( 'community' ),\\n\\t\\t'tags'    => array( 'outdoors' ),\\n\\t\\t'fill'    => 0,\\n\\t),\\n);\\n\\n\\\/\\\/ The table is empty, so the longest span is 0. Measuring it uses\\n\\\/\\\/ TIMESTAMPDIFF(), which Playground's SQLite driver does not know.\\nadd_option( \\\\HappenBoard\\\\Service\\\\Occurrences\\\\Repository::MAX_SPAN_OPTION, 0 );\\n\\n$hb_media      = array();\\n$hb_syncer     = new \\\\HappenBoard\\\\Service\\\\Events\\\\OccurrenceSyncer();\\n$hb_person_no  = 0;\\n$hb_attendees  = 0;\\n$hb_now_ts     = $hb_now->getTimestamp();\\n$hb_attendee_t = $wpdb->prefix . 'happenboard_attendees';\\n$hb_occ_t      = $wpdb->prefix . 'happenboard_occurrences';\\n\\nforeach ( $hb_events as $hb_def ) {\\n\\ttry {\\n\\t\\t$start = $hb_def['start'];\\n\\t\\t$end   = $hb_def['end'] ?? $start->modify( '+' . (int) $hb_def['minutes'] . ' minutes' );\\n\\n\\t\\t$created = min( $start->getTimestamp(), $hb_now_ts ) - mt_rand( 10, 40 ) * DAY_IN_SECONDS;\\n\\t\\t$hb_id      = wp_insert_post(\\n\\t\\t\\tarray(\\n\\t\\t\\t\\t'post_type'     => 'hboard_event',\\n\\t\\t\\t\\t'post_status'   => $hb_def['status'] ?? 'publish',\\n\\t\\t\\t\\t'post_title'    => $hb_def['title'],\\n\\t\\t\\t\\t'post_excerpt'  => $hb_def['excerpt'],\\n\\t\\t\\t\\t'post_content'  => $hb_paragraphs( $hb_def['content'] ),\\n\\t\\t\\t\\t'post_author'   => 1,\\n\\t\\t\\t\\t'post_date'     => wp_date( 'Y-m-d H:i:s', $created ),\\n\\t\\t\\t\\t'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $created ),\\n\\t\\t\\t),\\n\\t\\t\\ttrue\\n\\t\\t);\\n\\t\\tif ( is_wp_error( $hb_id ) ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\n\\t\\t$format = $hb_def['format'] ?? 'in_person';\\n\\t\\t$meta   = array(\\n\\t\\t\\t'_happenboard_start'        => $hb_iso( $start ),\\n\\t\\t\\t'_happenboard_end'          => $hb_iso( $end ),\\n\\t\\t\\t'_happenboard_all_day'      => ! empty( $hb_def['all_day'] ),\\n\\t\\t\\t'_happenboard_rrule'        => $hb_def['rrule'] ?? '',\\n\\t\\t\\t'_happenboard_capacity'     => (int) ( $hb_def['capacity'] ?? 0 ),\\n\\t\\t\\t'_happenboard_event_format' => $format,\\n\\t\\t);\\n\\t\\tif ( ! empty( $hb_def['venue'] ) ) {\\n\\t\\t\\t$meta['_happenboard_venue_id'] = $hb_venues[ $hb_def['venue'] ];\\n\\t\\t}\\n\\t\\tif ( ! empty( $hb_def['org'] ) ) {\\n\\t\\t\\t$meta['_happenboard_organizer_id'] = $hb_orgs[ $hb_def['org'] ];\\n\\t\\t}\\n\\t\\tif ( 'in_person' !== $format ) {\\n\\t\\t\\t$meta['_happenboard_online_provider'] = $hb_def['provider'];\\n\\t\\t\\t$meta['_happenboard_online_url']      = $hb_def['url'];\\n\\t\\t}\\n\\t\\tif ( isset( $hb_def['cancel'] ) ) {\\n\\t\\t\\t\\\/\\\/ Exceptions are keyed by the date's start as the rule writes it.\\n\\t\\t\\t$upcoming = $start;\\n\\t\\t\\twhile ( $upcoming < $hb_now ) {\\n\\t\\t\\t\\t$upcoming = $upcoming->modify( '+7 days' );\\n\\t\\t\\t}\\n\\t\\t\\t$cancelled                       = $upcoming->modify( '+' . ( 7 * (int) $hb_def['cancel'] ) . ' days' );\\n\\t\\t\\t$meta['_happenboard_exceptions'] = wp_json_encode( array( $hb_iso( $cancelled ) => array( 'cancelled' => true ) ) );\\n\\t\\t}\\n\\t\\tforeach ( $meta as $key => $value ) {\\n\\t\\t\\tupdate_post_meta( $hb_id, $key, $value );\\n\\t\\t}\\n\\n\\t\\twp_set_object_terms( $hb_id, array_values( array_intersect_key( $hb_cats, array_flip( $hb_def['cats'] ) ) ), 'happenboard_category' );\\n\\t\\twp_set_object_terms( $hb_id, array_values( array_intersect_key( $hb_tags, array_flip( $hb_def['tags'] ) ) ), 'happenboard_tag' );\\n\\n\\t\\tif ( ! empty( $hb_def['photo'] ) ) {\\n\\t\\t\\t$photo = $hb_photo( ...$hb_def['photo'] );\\n\\t\\t\\tif ( $photo ) {\\n\\t\\t\\t\\t$hb_media[ $hb_def['photo'][1] ] = $photo;\\n\\t\\t\\t\\tset_post_thumbnail( $hb_id, $photo );\\n\\t\\t\\t\\twp_update_post(\\n\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t'ID'          => $photo,\\n\\t\\t\\t\\t\\t\\t'post_parent' => $hb_id,\\n\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t);\\n\\t\\t\\t}\\n\\t\\t}\\n\\n\\t\\tif ( 'publish' !== ( $hb_def['status'] ?? 'publish' ) ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\n\\t\\t$hb_syncer->sync( (int) $hb_id );\\n\\n\\t\\tif ( empty( $hb_def['fill'] ) ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\n\\t\\t\\\/\\\/ RSVPs on every date that has happened or is coming up soon.\\n\\t\\t$occurrences = $wpdb->get_results( $wpdb->prepare( 'SELECT occurrence_id, sequence, start_utc FROM %i WHERE event_id = %d AND is_cancelled = 0 ORDER BY start_utc ASC', $hb_occ_t, $hb_id ), ARRAY_A ); \\\/\\\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery\\n\\t\\t$capacity    = (int) ( $hb_def['capacity'] ?? 0 );\\n\\t\\t$recurring   = '' !== ( $hb_def['rrule'] ?? '' );\\n\\n\\t\\tforeach ( (array) $occurrences as $occ ) {\\n\\t\\t\\t$occ_ts   = strtotime( $occ['start_utc'] . ' UTC' );\\n\\t\\t\\t$days_out = ( $occ_ts - $hb_now_ts ) \\\/ DAY_IN_SECONDS;\\n\\t\\t\\tif ( $days_out > 45 && $recurring ) {\\n\\t\\t\\t\\tbreak;\\n\\t\\t\\t}\\n\\t\\t\\t$fill  = (float) $hb_def['fill'] * ( $days_out > 21 ? 0.4 : ( $days_out > 7 ? 0.75 : 1.0 ) );\\n\\t\\t\\t$room  = $capacity > 0 ? $capacity : (int) ( $hb_def['people'] ?? 40 );\\n\\t\\t\\t$seats = (int) round( min( $room, (int) ( $hb_def['people'] ?? $room ) ) * $fill * ( mt_rand( 85, 100 ) \\\/ 100 ) );\\n\\t\\t\\tif ( 1.0 === (float) $hb_def['fill'] ) {\\n\\t\\t\\t\\t$seats = $room;\\n\\t\\t\\t}\\n\\t\\t\\t$going = 0;\\n\\t\\t\\t$queue = (int) ( $hb_def['waitlist'] ?? 0 );\\n\\n\\t\\t\\twhile ( $going < $seats || $queue > 0 ) {\\n\\t\\t\\t\\t[ $name, $email ] = $hb_person( ++$hb_person_no );\\n\\n\\t\\t\\t\\tif ( $going < $seats ) {\\n\\t\\t\\t\\t\\t$qty    = mt_rand( 0, 99 ) < 20 ? 2 : 1;\\n\\t\\t\\t\\t\\t$qty    = min( $qty, $seats - $going );\\n\\t\\t\\t\\t\\t$roll   = mt_rand( 0, 99 );\\n\\t\\t\\t\\t\\t$hb_status = $roll < 6 ? 'cancelled' : ( $roll < 9 ? 'declined' : 'going' );\\n\\t\\t\\t\\t\\tif ( 'going' === $hb_status ) {\\n\\t\\t\\t\\t\\t\\t$going += $qty;\\n\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t} else {\\n\\t\\t\\t\\t\\t$qty    = 1;\\n\\t\\t\\t\\t\\t$hb_status = 'waitlist';\\n\\t\\t\\t\\t\\t--$queue;\\n\\t\\t\\t\\t}\\n\\n\\t\\t\\t\\t$booked = min( $occ_ts, $hb_now_ts ) - mt_rand( 1, 20 ) * DAY_IN_SECONDS - mt_rand( 0, 86399 );\\n\\t\\t\\t\\t$wpdb->insert( \\\/\\\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery\\n\\t\\t\\t\\t\\t$hb_attendee_t,\\n\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t'event_id'             => $hb_id,\\n\\t\\t\\t\\t\\t\\t'occurrence_id'        => (int) $occ['occurrence_id'],\\n\\t\\t\\t\\t\\t\\t'occurrence_seq'       => $recurring ? (int) $occ['sequence'] : null,\\n\\t\\t\\t\\t\\t\\t'occurrence_start_utc' => $recurring ? (string) $occ['start_utc'] : null,\\n\\t\\t\\t\\t\\t\\t'email'                => $email,\\n\\t\\t\\t\\t\\t\\t'display_name'         => $name,\\n\\t\\t\\t\\t\\t\\t'status'               => $hb_status,\\n\\t\\t\\t\\t\\t\\t'quantity'             => $qty,\\n\\t\\t\\t\\t\\t\\t'cancel_token'         => bin2hex( random_bytes( 32 ) ),\\n\\t\\t\\t\\t\\t\\t'created_utc'          => gmdate( 'Y-m-d H:i:s', $booked ),\\n\\t\\t\\t\\t\\t\\t'updated_utc'          => gmdate( 'Y-m-d H:i:s', $booked ),\\n\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t);\\n\\t\\t\\t\\t++$hb_attendees;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t} catch ( \\\\Throwable $e ) {\\n\\t\\terror_log( 'HappenBoard demo: ' . $hb_def['title'] . ': ' . $e->getMessage() ); \\\/\\\/ phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log\\n\\t}\\n}\\n\\n\\\/* ------------------------------------------------------------ booking *\\\/\\n\\n$hb_rules     = new \\\\HappenBoard\\\\Service\\\\Booking\\\\Availability\\\\RuleRepository();\\n$hb_booking_t = $wpdb->prefix . 'happenboard_bookings';\\n\\n$hb_types = array(\\n\\tarray(\\n\\t\\t'title'    => '1:1 Career Coaching',\\n\\t\\t'excerpt'  => 'A focused 45-minute session to work on your CV, an interview or your next career move.',\\n\\t\\t'content'  => array(\\n\\t\\t\\t'Maya has coached more than 400 people through career changes. Tell her where you are and where you want to go, and leave with a clear next step.',\\n\\t\\t\\t'Sessions take place at The Loft Studio. You pay on the day.',\\n\\t\\t),\\n\\t\\t'duration' => 45,\\n\\t\\t'capacity' => 1,\\n\\t\\t'price'    => 6500,\\n\\t\\t'mode'     => 'in_person',\\n\\t\\t'color'    => '#7c3aed',\\n\\t\\t'hosts'    => array( $hb_maya ),\\n\\t\\t'notice'   => 720,\\n\\t\\t'advance'  => 60,\\n\\t\\t'step'     => 15,\\n\\t\\t'photo'    => array( '1573497620053-ea5300f94f21', 'career-coaching', 'Two women talking across a small table' ),\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Free Discovery Call',\\n\\t\\t'excerpt'  => 'Thirty minutes on video to see whether coaching is right for you. No commitment.',\\n\\t\\t'content'  => array( 'A relaxed first conversation on video. We talk about your goals and see together whether coaching can help.' ),\\n\\t\\t'duration' => 30,\\n\\t\\t'capacity' => 1,\\n\\t\\t'price'    => 0,\\n\\t\\t'mode'     => 'virtual',\\n\\t\\t'color'    => '#0ea5e9',\\n\\t\\t'hosts'    => array( $hb_maya ),\\n\\t\\t'notice'   => 120,\\n\\t\\t'advance'  => 30,\\n\\t\\t'step'     => 30,\\n\\t\\t'photo'    => array( '1551836022-d5d88e9218df', 'discovery-call', 'Two women talking in front of a laptop in a bright office' ),\\n\\t),\\n\\tarray(\\n\\t\\t'title'    => 'Pottery Wheel Taster',\\n\\t\\t'excerpt'  => 'A 90-minute small-group session to try the wheel before you sign up for a course.',\\n\\t\\t'content'  => array( 'Up to six people per session. Our potters show you how to center the clay and throw your first bowl, which we fire and keep for you to collect.' ),\\n\\t\\t'duration' => 90,\\n\\t\\t'capacity' => 6,\\n\\t\\t'price'    => 3500,\\n\\t\\t'mode'     => 'in_person',\\n\\t\\t'color'    => '#ea580c',\\n\\t\\t'hosts'    => array( $hb_daniel, 1 ),\\n\\t\\t'notice'   => 1440,\\n\\t\\t'advance'  => 45,\\n\\t\\t'step'     => 30,\\n\\t\\t'photo'    => array( '1508269151431-a34449ca161d', 'pottery-taster', 'Hands carving small clay bowls on a workbench' ),\\n\\t),\\n);\\n\\n$hb_bookings = 0;\\nforeach ( $hb_types as $hb_def ) {\\n\\ttry {\\n\\t\\t$hb_id = $hb_post(\\n\\t\\t\\t'hboard_booking_type',\\n\\t\\t\\t$hb_def['title'],\\n\\t\\t\\t$hb_def['excerpt'],\\n\\t\\t\\tarray(\\n\\t\\t\\t\\t'_happenboard_bt_duration'      => $hb_def['duration'],\\n\\t\\t\\t\\t'_happenboard_bt_capacity'      => $hb_def['capacity'],\\n\\t\\t\\t\\t'_happenboard_bt_buffer_before' => 0,\\n\\t\\t\\t\\t'_happenboard_bt_buffer_after'  => 15,\\n\\t\\t\\t\\t'_happenboard_bt_min_notice'    => $hb_def['notice'],\\n\\t\\t\\t\\t'_happenboard_bt_max_advance'   => $hb_def['advance'],\\n\\t\\t\\t\\t'_happenboard_bt_increment'     => $hb_def['step'],\\n\\t\\t\\t\\t'_happenboard_bt_daily_cap'     => 0,\\n\\t\\t\\t\\t'_happenboard_bt_price_cents'   => $hb_def['price'],\\n\\t\\t\\t\\t'_happenboard_bt_deposit_cents' => 0,\\n\\t\\t\\t\\t'_happenboard_bt_assignment'    => count( $hb_def['hosts'] ) > 1 ? 'round_robin' : 'fixed',\\n\\t\\t\\t\\t'_happenboard_bt_location_mode' => $hb_def['mode'],\\n\\t\\t\\t\\t'_happenboard_bt_color'         => $hb_def['color'],\\n\\t\\t\\t\\t'_happenboard_bt_host_ids'      => $hb_def['hosts'],\\n\\t\\t\\t),\\n\\t\\t\\t$hb_paragraphs( $hb_def['content'] )\\n\\t\\t);\\n\\t\\tif ( ! $hb_id ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\t\\t$photo = $hb_photo( ...$hb_def['photo'] );\\n\\t\\tif ( $photo ) {\\n\\t\\t\\tset_post_thumbnail( $hb_id, $photo );\\n\\t\\t}\\n\\n\\t\\t\\\/\\\/ Weekdays 9\\u201312 and 2\\u20136 pm; the group taster also runs Saturday mornings.\\n\\t\\tforeach ( $hb_def['hosts'] as $host ) {\\n\\t\\t\\tforeach ( array( 1, 2, 3, 4, 5 ) as $day ) {\\n\\t\\t\\t\\tforeach ( array( array( 540, 720 ), array( 840, 1080 ) ) as $span ) {\\n\\t\\t\\t\\t\\t$hb_rules->insert(\\n\\t\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t\\t'host_user_id'    => $host,\\n\\t\\t\\t\\t\\t\\t\\t'booking_type_id' => $hb_id,\\n\\t\\t\\t\\t\\t\\t\\t'rule_type'       => 'weekly',\\n\\t\\t\\t\\t\\t\\t\\t'week_day'        => $day,\\n\\t\\t\\t\\t\\t\\t\\t'start_min'       => $span[0],\\n\\t\\t\\t\\t\\t\\t\\t'end_min'         => $span[1],\\n\\t\\t\\t\\t\\t\\t\\t'is_available'    => 1,\\n\\t\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t\\t);\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t\\tif ( $hb_def['capacity'] > 1 ) {\\n\\t\\t\\t\\t$hb_rules->insert(\\n\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t'host_user_id'    => $host,\\n\\t\\t\\t\\t\\t\\t'booking_type_id' => $hb_id,\\n\\t\\t\\t\\t\\t\\t'rule_type'       => 'weekly',\\n\\t\\t\\t\\t\\t\\t'week_day'        => 6,\\n\\t\\t\\t\\t\\t\\t'start_min'       => 600,\\n\\t\\t\\t\\t\\t\\t'end_min'         => 780,\\n\\t\\t\\t\\t\\t\\t'is_available'    => 1,\\n\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t);\\n\\t\\t\\t}\\n\\t\\t}\\n\\n\\t\\t\\\/\\\/ Five past and five upcoming bookings per type, on open weekdays.\\n\\t\\t$used = array();\\n\\t\\tforeach ( array( -24, -17, -11, -6, -2, 2, 3, 6, 9, 15 ) as $n => $offset ) {\\n\\t\\t\\t$day = $hb_today->modify( ( $offset > 0 ? '+' : '' ) . $offset . ' days' );\\n\\t\\t\\twhile ( (int) $day->format( 'N' ) > 5 ) {\\n\\t\\t\\t\\t$day = $day->modify( $offset > 0 ? '+1 day' : '-1 day' );\\n\\t\\t\\t}\\n\\t\\t\\t$hour  = array( 9, 10, 11, 14, 15, 16 )[ ( $n + $hb_def['duration'] ) % 6 ];\\n\\t\\t\\t$start = $day->setTime( $hour, 0 );\\n\\t\\t\\t$host  = $hb_def['hosts'][ $n % count( $hb_def['hosts'] ) ];\\n\\t\\t\\tif ( isset( $used[ $host . $start->format( 'c' ) ] ) ) {\\n\\t\\t\\t\\tcontinue;\\n\\t\\t\\t}\\n\\t\\t\\t$used[ $host . $start->format( 'c' ) ] = true;\\n\\n\\t\\t\\tif ( $offset < 0 ) {\\n\\t\\t\\t\\t$hb_status = 3 === $n ? 'no_show' : ( 1 === $n ? 'cancelled' : 'completed' );\\n\\t\\t\\t} else {\\n\\t\\t\\t\\t$hb_status = 8 === $n && $hb_def['price'] > 0 ? 'pending_approval' : 'confirmed';\\n\\t\\t\\t}\\n\\t\\t\\t[ $name, $email ] = $hb_person( ++$hb_person_no );\\n\\t\\t\\t$booked           = min( $start->getTimestamp(), $hb_now_ts ) - mt_rand( 2, 12 ) * DAY_IN_SECONDS;\\n\\n\\t\\t\\t$wpdb->insert( \\\/\\\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery\\n\\t\\t\\t\\t$hb_booking_t,\\n\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t'booking_type_id' => $hb_id,\\n\\t\\t\\t\\t\\t'host_user_id'    => $host,\\n\\t\\t\\t\\t\\t'customer_email'  => $email,\\n\\t\\t\\t\\t\\t'customer_name'   => $name,\\n\\t\\t\\t\\t\\t'customer_tz'     => 'America\\\/Los_Angeles',\\n\\t\\t\\t\\t\\t'start_utc'       => $hb_sql( $start ),\\n\\t\\t\\t\\t\\t'end_utc'         => $hb_sql( $start->modify( '+' . $hb_def['duration'] . ' minutes' ) ),\\n\\t\\t\\t\\t\\t'status'          => $hb_status,\\n\\t\\t\\t\\t\\t'quantity'        => $hb_def['capacity'] > 1 ? 1 + ( $n % 3 ) : 1,\\n\\t\\t\\t\\t\\t'price_cents'     => $hb_def['price'] > 0 ? $hb_def['price'] * ( $hb_def['capacity'] > 1 ? 1 + ( $n % 3 ) : 1 ) : null,\\n\\t\\t\\t\\t\\t'currency'        => 'USD',\\n\\t\\t\\t\\t\\t'payment_status'  => 'none',\\n\\t\\t\\t\\t\\t'cancel_token'    => bin2hex( random_bytes( 32 ) ),\\n\\t\\t\\t\\t\\t'created_utc'     => gmdate( 'Y-m-d H:i:s', $booked ),\\n\\t\\t\\t\\t\\t'updated_utc'     => gmdate( 'Y-m-d H:i:s', $booked ),\\n\\t\\t\\t\\t)\\n\\t\\t\\t);\\n\\t\\t\\t++$hb_bookings;\\n\\t\\t}\\n\\t} catch ( \\\\Throwable $e ) {\\n\\t\\terror_log( 'HappenBoard demo: ' . $hb_def['title'] . ': ' . $e->getMessage() ); \\\/\\\/ phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log\\n\\t}\\n}\\n\\n\\\/\\\/ Maya takes a long weekend off in three weeks.\\n$hb_off = $hb_next_weekday( $hb_today->modify( '+21 days' ), 5 );\\nforeach ( array( 0, 3 ) as $shift ) {\\n\\t$hb_rules->insert(\\n\\t\\tarray(\\n\\t\\t\\t'host_user_id'  => $hb_maya,\\n\\t\\t\\t'rule_type'     => 'day_off',\\n\\t\\t\\t'specific_date' => $hb_off->modify( '+' . $shift . ' days' )->format( 'Y-m-d' ),\\n\\t\\t\\t'is_available'  => 0,\\n\\t\\t)\\n\\t);\\n}\\n\\n\\\/* --------------------------------------------------------------- pages *\\\/\\n\\n$hb_page = static function ( string $title, string $content, string $slug ): int {\\n\\t$existing = get_page_by_path( $slug );\\n\\tif ( $existing ) {\\n\\t\\treturn (int) $existing->ID;\\n\\t}\\n\\treturn (int) wp_insert_post(\\n\\t\\tarray(\\n\\t\\t\\t'post_type'    => 'page',\\n\\t\\t\\t'post_status'  => 'publish',\\n\\t\\t\\t'post_title'   => $title,\\n\\t\\t\\t'post_name'    => $slug,\\n\\t\\t\\t'post_content' => $content,\\n\\t\\t\\t'post_author'  => 1,\\n\\t\\t)\\n\\t);\\n};\\n\\n\\\/\\\/ The home page: a cover photo, the next events and the booking types.\\n$hb_cover     = $hb_media['food-and-makers-festival'] ?? 0;\\n$hb_cover_url = $hb_cover ? (string) wp_get_attachment_image_url( $hb_cover, 'full' ) : '';\\n$hb_cover_img = $hb_cover_url\\n\\t? sprintf( '<img class=\\\"wp-block-cover__image-background wp-image-%1$d\\\" alt=\\\"\\\" src=\\\"%2$s\\\" data-object-fit=\\\"cover\\\"\\\/>', $hb_cover, esc_url( $hb_cover_url ) )\\n\\t: '';\\n$hb_cover_att = $hb_cover_url ? sprintf( '\\\"url\\\":\\\"%1$s\\\",\\\"id\\\":%2$d,', esc_url( $hb_cover_url ), $hb_cover ) : '';\\n\\n$hb_home = $hb_page(\\n\\t'Home',\\n\\t'<!-- wp:cover {' . $hb_cover_att . '\\\"dimRatio\\\":60,\\\"overlayColor\\\":\\\"black\\\",\\\"isUserOverlayColor\\\":true,\\\"minHeight\\\":520,\\\"align\\\":\\\"full\\\",\\\"layout\\\":{\\\"type\\\":\\\"constrained\\\"}} -->\\n<div class=\\\"wp-block-cover alignfull\\\" style=\\\"min-height:520px\\\"><span aria-hidden=\\\"true\\\" class=\\\"wp-block-cover__background has-black-background-color has-background-dim-60 has-background-dim\\\"><\\\/span>' . $hb_cover_img . '<div class=\\\"wp-block-cover__inner-container\\\"><!-- wp:heading {\\\"textAlign\\\":\\\"center\\\",\\\"level\\\":1} -->\\n<h1 class=\\\"wp-block-heading has-text-align-center\\\">Something on every week at Riverside<\\\/h1>\\n<!-- \\\/wp:heading -->\\n\\n<!-- wp:paragraph {\\\"align\\\":\\\"center\\\",\\\"fontSize\\\":\\\"large\\\"} -->\\n<p class=\\\"has-text-align-center has-large-font-size\\\">Concerts, classes, meetups and family days in Portland. Save a seat in two clicks.<\\\/p>\\n<!-- \\\/wp:paragraph -->\\n\\n<!-- wp:buttons {\\\"layout\\\":{\\\"type\\\":\\\"flex\\\",\\\"justifyContent\\\":\\\"center\\\"}} -->\\n<div class=\\\"wp-block-buttons\\\"><!-- wp:button -->\\n<div class=\\\"wp-block-button\\\"><a class=\\\"wp-block-button__link wp-element-button\\\" href=\\\"' . esc_url( home_url( '\\\/calendar\\\/' ) ) . '\\\">See the calendar<\\\/a><\\\/div>\\n<!-- \\\/wp:button -->\\n\\n<!-- wp:button {\\\"className\\\":\\\"is-style-outline\\\"} -->\\n<div class=\\\"wp-block-button is-style-outline\\\"><a class=\\\"wp-block-button__link wp-element-button\\\" href=\\\"' . esc_url( home_url( '\\\/book\\\/' ) ) . '\\\">Book a session<\\\/a><\\\/div>\\n<!-- \\\/wp:button --><\\\/div>\\n<!-- \\\/wp:buttons --><\\\/div><\\\/div>\\n<!-- \\\/wp:cover -->\\n\\n<!-- wp:heading {\\\"align\\\":\\\"wide\\\"} -->\\n<h2 class=\\\"wp-block-heading alignwide\\\">Coming up<\\\/h2>\\n<!-- \\\/wp:heading -->\\n\\n<!-- wp:happenboard\\\/events-grid {\\\"count\\\":6,\\\"columns\\\":3,\\\"align\\\":\\\"wide\\\"} \\\/-->\\n\\n<!-- wp:heading {\\\"align\\\":\\\"wide\\\"} -->\\n<h2 class=\\\"wp-block-heading alignwide\\\">Book a one-to-one<\\\/h2>\\n<!-- \\\/wp:heading -->\\n\\n<!-- wp:happenboard\\\/booking-types-list {\\\"columns\\\":3,\\\"align\\\":\\\"wide\\\"} \\\/-->',\\n\\t'home'\\n);\\nupdate_post_meta( $hb_home, '_wp_page_template', 'page-no-title' );\\n$hb_page(\\n\\t'Calendar',\\n\\t'<!-- wp:happenboard\\\/calendar \\\/-->',\\n\\t'calendar'\\n);\\n$hb_page(\\n\\t'Book a session',\\n\\t'<!-- wp:paragraph -->\\n<p>Pick a session, choose a time that suits you and you are booked. You get a confirmation email with a calendar invite.<\\\/p>\\n<!-- \\\/wp:paragraph -->\\n\\n<!-- wp:happenboard\\\/booking-types-list \\\/-->',\\n\\t'book'\\n);\\n$hb_page(\\n\\t'My schedule',\\n\\t'<!-- wp:happenboard\\\/my-schedule \\\/-->',\\n\\t'my-schedule'\\n);\\n\\nif ( $hb_home ) {\\n\\tupdate_option( 'show_on_front', 'page' );\\n\\tupdate_option( 'page_on_front', $hb_home );\\n}\\n\\n\\\/\\\/ The sample content WordPress installs has nothing to do with events.\\nforeach ( array(\\n\\t'hello-world' => 'post',\\n\\t'sample-page' => 'page',\\n) as $hb_slug => $hb_type ) {\\n\\t$hb_sample = get_page_by_path( $hb_slug, OBJECT, $hb_type );\\n\\tif ( $hb_sample ) {\\n\\t\\twp_delete_post( $hb_sample->ID, true );\\n\\t}\\n}\\n\\nflush_rewrite_rules( false );\\n\\necho esc_html( sprintf( 'HappenBoard demo: %d attendees, %d bookings.', $hb_attendees, $hb_bookings ) );\\n\"},{\"step\":\"login\",\"username\":\"admin\"}]}"}},"all_blocks":{"happenboard\/favorite-toggle":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/favorite-toggle","title":"Save Event Button","category":"happenboard","description":"A heart \/ bookmark toggle so visitors can save an event to their personal schedule. Persists in their account when logged in, in localStorage otherwise.","icon":"heart","keywords":["favorite","save","bookmark","wishlist","schedule"],"supports":{"html":false,"spacing":{"margin":true,"padding":true},"color":{"background":true,"text":true},"typography":{"fontSize":true}},"attributes":{"saveLabel":{"type":"string","default":"Save event"},"iconStyle":{"type":"string","default":"heart","enum":["heart","bookmark","star"]}},"usesContext":["postId","postType"],"textdomain":"happenboard","editorScript":"file:.\/index.js","viewScript":"file:.\/view.js","style":"file:.\/style-index.css"},"happenboard\/event-description":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-description","title":"Event Description","description":"Renders the current event's content with the standard the_content filter applied.","category":"happenboard","icon":"editor-paragraph","keywords":["event","description","content","body"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showExcerptOnly":{"type":"boolean","default":false}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js"},"happenboard\/event-query-loop":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-query-loop","title":"Event Query Loop","description":"Filterable server-rendered loop of events by date range, category, tag, venue, or organizer. The events-flavoured Query Loop.","category":"happenboard","icon":"feedback","keywords":["events","query","loop","filter","list","grid"],"textdomain":"happenboard","version":"1.1.0","attributes":{"count":{"type":"number","default":10},"dateRange":{"type":"string","default":"upcoming"},"dateFrom":{"type":"string","default":""},"dateTo":{"type":"string","default":""},"search":{"type":"string","default":""},"order":{"type":"string","default":""},"categoryIds":{"type":"array","default":[],"items":{"type":"number"}},"tagIds":{"type":"array","default":[],"items":{"type":"number"}},"venueId":{"type":"number","default":0},"organizerId":{"type":"number","default":0},"layout":{"type":"string","default":"list"},"columns":{"type":"number","default":3},"showImage":{"type":"boolean","default":true},"showDate":{"type":"boolean","default":true},"showVenue":{"type":"boolean","default":true},"showOrganizer":{"type":"boolean","default":false},"showCategories":{"type":"boolean","default":false},"showExcerpt":{"type":"boolean","default":false},"headingLevel":{"type":"number","default":2},"onePerEvent":{"type":"boolean","default":false},"offset":{"type":"number","default":0}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/booking-page":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/booking-page","title":"Booking Page","description":"A self-service appointment-booking flow: pick a time, fill your details, get confirmed. FSE-native, no iframe.","category":"happenboard","icon":"calendar-alt","keywords":["booking","appointment","schedule","calendar"],"textdomain":"happenboard","version":"1.1.0","attributes":{"bookingTypeId":{"type":"number","default":0},"introText":{"type":"string","default":""}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","viewScript":"file:.\/view.js","style":"file:.\/style-index.css"},"happenboard\/booking-types-list":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/booking-types-list","title":"Booking Types","description":"A catalogue of bookable services. Each card links to that service's own booking page.","category":"happenboard","icon":"list-view","keywords":["booking","services","appointments","catalogue"],"textdomain":"happenboard","version":"1.1.0","attributes":{"columns":{"type":"number","default":2},"showDuration":{"type":"boolean","default":true},"showDescription":{"type":"boolean","default":true}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/my-schedule":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/my-schedule","title":"My Schedule","category":"happenboard","description":"Lists the events the current visitor has saved. Empty state when nothing is saved. Designed for a dedicated \"My schedule\" page that visitors can bookmark.","icon":"calendar","keywords":["favorites","wishlist","schedule","saved","personal"],"supports":{"html":false,"align":["wide","full"],"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"attributes":{"showImage":{"type":"boolean","default":true},"showDate":{"type":"boolean","default":true},"showVenue":{"type":"boolean","default":true},"emptyTitle":{"type":"string","default":"No saved events yet"},"emptyText":{"type":"string","default":"Tap the Save button on any event to add it here."}},"textdomain":"happenboard","editorScript":"file:.\/index.js","viewScript":"file:.\/view.js","style":"file:.\/style-index.css"},"happenboard\/upcoming-events":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/upcoming-events","title":"Upcoming Events","description":"Display a server-rendered list of upcoming events.","category":"happenboard","icon":"calendar-alt","keywords":["events","calendar","schedule","upcoming"],"textdomain":"happenboard","version":"1.1.0","attributes":{"count":{"type":"number","default":5}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","editorStyle":"file:.\/index.css","style":"file:.\/style-index.css"},"happenboard\/event-organizer":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-organizer","title":"Event Organizer","description":"Display the organizer name and contact for the current event.","category":"happenboard","icon":"businessperson","keywords":["event","organizer","host","contact"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showEmail":{"type":"boolean","default":true},"showLabel":{"type":"boolean","default":false}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/event-venue":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-venue","title":"Event Venue","description":"Display the venue name and address for the current event.","category":"happenboard","icon":"location-alt","keywords":["event","venue","location","address"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showAddress":{"type":"boolean","default":true},"showLabel":{"type":"boolean","default":false},"showMap":{"type":"boolean","default":false},"mapHeight":{"type":"number","default":280},"mapZoom":{"type":"number","default":15},"mapProvider":{"type":"string","default":"site"}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/my-bookings":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/my-bookings","title":"My Bookings","category":"happenboard","description":"Lists the appointments the signed-in visitor booked, with a cancel action on upcoming ones. Signed-out visitors get a sign-in prompt. Bookings made without an account are not listed \u2014 those are managed through the link in the confirmation email.","icon":"clipboard","keywords":["bookings","appointments","account","cancel","customer"],"supports":{"html":false,"align":["wide"],"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"attributes":{"showPast":{"type":"boolean","default":true}},"textdomain":"happenboard","editorScript":"file:.\/index.js","viewScript":"file:.\/view.js","style":"file:.\/style-index.css"},"happenboard\/event-date-time":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-date-time","title":"Event Date & Time","description":"Display the next occurrence date and time for the current event.","category":"happenboard","icon":"clock","keywords":["event","date","time","schedule"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showEnd":{"type":"boolean","default":true},"showTimezone":{"type":"boolean","default":false}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true},"typography":{"fontSize":true,"lineHeight":true,"fontStyle":true,"fontWeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/event-weather":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-weather","title":"Event Weather","description":"Show the weather for the event's date at its venue \u2014 forecast, observed history, or a typical-for-the-season normal.","category":"happenboard","icon":"cloud","keywords":["event","weather","forecast","temperature","meteo"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"display":{"type":"string","default":"detailed"},"tempUnit":{"type":"string","default":"celsius"},"windUnit":{"type":"string","default":"kmh"},"showCondition":{"type":"boolean","default":true},"showPrecip":{"type":"boolean","default":true},"showWind":{"type":"boolean","default":true},"showHumidity":{"type":"boolean","default":false},"showSun":{"type":"boolean","default":false},"latitude":{"type":"number","default":0},"longitude":{"type":"number","default":0},"dateOverride":{"type":"string","default":""}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true},"__experimentalBorder":{"color":true,"radius":true,"style":true,"width":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/events-list":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/events-list","title":"Events List","description":"Server-rendered list of upcoming events with date, venue, and a link to each event.","category":"happenboard","icon":"list-view","keywords":["events","list","schedule","upcoming","agenda"],"textdomain":"happenboard","version":"1.1.0","attributes":{"count":{"type":"number","default":10},"showVenue":{"type":"boolean","default":true},"showOrganizer":{"type":"boolean","default":false},"groupByMonth":{"type":"boolean","default":true},"headingLevel":{"type":"number","default":3},"onePerEvent":{"type":"boolean","default":false},"moreLink":{"type":"boolean","default":false}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/events-grid":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/events-grid","title":"Events Grid","description":"Server-rendered grid of upcoming events with featured image, date, venue, and a link to each event.","category":"happenboard","icon":"grid-view","keywords":["events","grid","cards","upcoming","tiles"],"textdomain":"happenboard","version":"1.1.0","attributes":{"count":{"type":"number","default":6},"columns":{"type":"number","default":3},"showImage":{"type":"boolean","default":true},"showDate":{"type":"boolean","default":true},"showVenue":{"type":"boolean","default":true},"showExcerpt":{"type":"boolean","default":false},"headingLevel":{"type":"number","default":2}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/rsvp-form":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/rsvp-form","title":"RSVP Form","description":"Collect free RSVPs for the current event. Capacity-aware, no extra plugin required.","category":"happenboard","icon":"yes-alt","keywords":["events","rsvp","registration","form"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showCapacity":{"type":"boolean","default":true},"allowQuantity":{"type":"boolean","default":false},"maxQuantity":{"type":"number","default":4},"successMessage":{"type":"string","default":""}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","viewScript":"file:.\/view.js","style":"file:.\/style-index.css"},"happenboard\/event-series-occurrences":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-series-occurrences","title":"Event Series: All Dates","category":"happenboard","description":"Lists every upcoming occurrence of the current recurring event. Renders nothing for one-shot events. Used by the Series page template and the default Single Event template.","icon":"list-view","keywords":["event","series","recurring","occurrences","dates"],"supports":{"html":false,"align":["wide","full"],"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"attributes":{"limit":{"type":"number","default":24},"showRrule":{"type":"boolean","default":true},"groupByMonth":{"type":"boolean","default":true},"heading":{"type":"string","default":"All dates"}},"usesContext":["postId","postType"],"textdomain":"happenboard","editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/calendar":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/calendar","title":"Calendar","description":"Show your events in a calendar with Month, Week, Day and Agenda views, past dates included.","category":"happenboard","icon":"calendar","keywords":["events","calendar","month","schedule"],"textdomain":"happenboard","version":"1.1.0","attributes":{"defaultView":{"type":"string","default":"month"},"agendaDays":{"type":"number","default":30},"showViewSwitcher":{"type":"boolean","default":true},"showSubscribe":{"type":"boolean","default":false}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","editorStyle":"file:.\/index.css","style":"file:.\/style-index.css","viewScript":"file:.\/view.js"},"happenboard\/add-to-calendar":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/add-to-calendar","title":"Add to Calendar","description":"One-click links to add this event to Google, Outlook, Yahoo, or download an .ics file.","category":"happenboard","icon":"calendar-alt","keywords":["calendar","ics","google","outlook","save"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showGoogle":{"type":"boolean","default":true},"showOutlook":{"type":"boolean","default":true},"showYahoo":{"type":"boolean","default":false},"showIcal":{"type":"boolean","default":true}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"},"happenboard\/countdown":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/countdown","title":"Event Countdown","description":"Live countdown to the event's start time. Falls back to a static label when the event has started.","category":"happenboard","icon":"backup","keywords":["countdown","timer","live"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showDays":{"type":"boolean","default":true},"showHours":{"type":"boolean","default":true},"showMinutes":{"type":"boolean","default":true},"showSeconds":{"type":"boolean","default":true},"liveLabel":{"type":"string","default":"Event has started"},"endedLabel":{"type":"string","default":"Event has ended"}},"supports":{"html":false,"align":["wide"],"color":{"background":true,"text":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true,"fontWeight":true}},"editorScript":"file:.\/index.js","viewScript":"file:.\/view.js","style":"file:.\/style-index.css"},"happenboard\/booking-availability":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/booking-availability","title":"Booking Availability","description":"Publishes when a service is free \u2014 as a month calendar or a schedule list \u2014 anywhere on the site. Each time links straight into the booking page with that slot pre-selected.","category":"happenboard","icon":"clock","keywords":["availability","booking","appointment","schedule","slots"],"textdomain":"happenboard","version":"1.1.0","attributes":{"bookingTypeId":{"type":"number","default":0},"layout":{"type":"string","default":"calendar"},"daysAhead":{"type":"number","default":30},"showHeader":{"type":"boolean","default":true}},"supports":{"html":false,"align":["wide","full"],"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","viewScript":"file:.\/view.js","style":"file:.\/style-index.css"},"happenboard\/event-categories-tags":{"$schema":"https:\/\/schemas.wp.org\/trunk\/block.json","apiVersion":3,"name":"happenboard\/event-categories-tags","title":"Event Categories & Tags","description":"Show the current event's categories and tags as linked chips.","category":"happenboard","icon":"tag","keywords":["event","category","tag","taxonomy"],"textdomain":"happenboard","version":"1.1.0","usesContext":["postId","postType"],"attributes":{"showCategories":{"type":"boolean","default":true},"showTags":{"type":"boolean","default":true},"linked":{"type":"boolean","default":true}},"supports":{"html":false,"color":{"background":true,"text":true,"link":true},"spacing":{"padding":true,"margin":true,"blockGap":true},"typography":{"fontSize":true,"lineHeight":true}},"editorScript":"file:.\/index.js","style":"file:.\/style-index.css"}},"tagged_versions":["1.0","1.1"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[8132,269,416,1486,15480],"plugin_category":[40],"plugin_contributors":[80067],"plugin_business_model":[],"class_list":["post-313469","plugin","type-plugin","status-publish","hentry","plugin_tags-appointments","plugin_tags-booking","plugin_tags-calendar","plugin_tags-events","plugin_tags-rsvp","plugin_category-calendar-and-events","plugin_contributors-rainbowgeek","plugin_committers-rainbowgeek"],"banners":{"banner":"https:\/\/ps.w.org\/happenboard\/assets\/banner-772x250.png?rev=3715552","banner_2x":"https:\/\/ps.w.org\/happenboard\/assets\/banner-1544x500.png?rev=3715552","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/happenboard\/assets\/icon.svg?rev=3715552","icon":"https:\/\/ps.w.org\/happenboard\/assets\/icon.svg?rev=3715552","icon_2x":false,"generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>HappenBoard is an events calendar and appointment booking plugin built for the block editor and the Site Editor. Its blocks are rendered by your server and styled by your theme, and its admin screens use the components of WordPress itself.<\/p>\n\n<p>HappenBoard is made by the team behind <a href=\"https:\/\/www.seopress.org\/\">SEOPress<\/a> and <a href=\"https:\/\/www.mailerpress.com\/\">MailerPress<\/a>.<\/p>\n\n<h4>Events<\/h4>\n\n<ul>\n<li>One-off and recurring events: every week, every month on the second Tuesday, every year. Each date of a series has its own page, and you can skip the dates that do not take place.<\/li>\n<li>Venues and organizers, each with a page listing their upcoming events.<\/li>\n<li>Categories and tags, with archive pages.<\/li>\n<li>A weather forecast on the event page, for outdoor events (optional, from Open-Meteo).<\/li>\n<\/ul>\n\n<h4>RSVPs<\/h4>\n\n<ul>\n<li>Free registration with a seat limit. On a recurring event, each date has its own seats.<\/li>\n<li>A confirmation email with a calendar file, automatic reminders, and a link for attendees to cancel.<\/li>\n<li>An Attendees screen to search, edit, move to another date and export to CSV.<\/li>\n<li>Spam protection: a hidden field and a signed timestamp by default, Cloudflare Turnstile or Akismet if you turn them on.<\/li>\n<\/ul>\n\n<h4>Appointment booking<\/h4>\n\n<ul>\n<li>Booking types with their length, price (shown to the customer, paid on site), buffers and notice time.<\/li>\n<li>Weekly availability for each host, days off, and round-robin assignment between hosts.<\/li>\n<li>Time slots with several seats, for group sessions.<\/li>\n<li>A calendar invite with every confirmation, and a link for customers to cancel or move their booking.<\/li>\n<\/ul>\n\n<h4>Blocks and templates<\/h4>\n\n<ul>\n<li>21 blocks, including Calendar, Events List, Events Grid, Event Query Loop, RSVP Form and Booking Page.<\/li>\n<li>11 patterns.<\/li>\n<li>Site Editor templates for single events, the events archive, category and tag archives, venue and organizer pages, booking types and the \"All dates\" page of a series. Classic themes get event pages and archives too.<\/li>\n<li>8 Elementor widgets for sites built with Elementor.<\/li>\n<li>Save Event Button and My Schedule blocks, so visitors can keep a list of the events they like.<\/li>\n<\/ul>\n\n<h4>Search engines<\/h4>\n\n<ul>\n<li>Event schema (JSON-LD) on every event page, so your events can appear in Google's event results.<\/li>\n<li>The event content is in the page's HTML, not loaded later by JavaScript.<\/li>\n<\/ul>\n\n<h4>Moving from another plugin<\/h4>\n\n<p>Import your events from The Events Calendar, Events Manager, Sugar Calendar, EventKoi, Amelia or any <code>.ics<\/code> file, with the venues, organizers, attendees and bookings the old plugin stored. The import works even if the old plugin is already deactivated.<\/p>\n\n<h4>AI agents<\/h4>\n\n<p>Connect Claude, ChatGPT, Cursor or any MCP client by pasting one address and signing in with your WordPress account. The agent can then find, create and update your events and bookings with your WordPress permissions. It cannot delete anything.<\/p>\n\n<h4>Also included<\/h4>\n\n<ul>\n<li>WPML and Polylang support (<code>wpml-config.xml<\/code> is bundled).<\/li>\n<li>The WordPress tools to export and erase personal data cover RSVPs, bookings and saved events.<\/li>\n<li>Site Health checks, for example when WP-Cron stops running and reminders would not be sent.<\/li>\n<li>A REST API and WP-CLI commands to create events and run imports.<\/li>\n<li>iCal feeds for the whole site and for each event.<\/li>\n<\/ul>\n\n<h4>For developers<\/h4>\n\n<ul>\n<li>PHP 8.2 with strict types and PSR-4 autoloading.<\/li>\n<li>Bundled libraries (sabre\/vobject) are prefixed, so they never clash with another plugin that ships the same library.<\/li>\n<li>Checked with PHPCS (WordPress Coding Standards), PHPStan and PHPUnit.<\/li>\n<li>Text domain <code>happenboard<\/code>.<\/li>\n<\/ul>\n\n<h3>External services<\/h3>\n\n<p>HappenBoard contacts no outside service on a fresh install. Each service below is used only after you turn it on.<\/p>\n\n<ul>\n<li><strong>HappenBoard newsletter<\/strong> (happenboard.com): when you subscribe on the last screen of the setup wizard, your email address is sent to happenboard.com. <a href=\"https:\/\/happenboard.com\/terms-and-conditions\/\">Terms<\/a>, <a href=\"https:\/\/happenboard.com\/privacy-policy\/\">privacy<\/a>.<\/li>\n<li><strong>Open-Meteo<\/strong> (Event Weather block): your server sends the venue's coordinates and the event dates to get the weather. No visitor data is sent. <a href=\"https:\/\/open-meteo.com\/en\/terms\">Terms and privacy<\/a>.<\/li>\n<li><strong>OpenStreetMap<\/strong> or <strong>Google Maps<\/strong> (venue maps, when \"Show map\" is on): the visitor's browser loads the map with the venue's coordinates, and Google Maps also receives your API key. <a href=\"https:\/\/osmfoundation.org\/wiki\/Terms_of_Use\">OpenStreetMap terms<\/a>, <a href=\"https:\/\/osmfoundation.org\/wiki\/Privacy_Policy\">privacy<\/a>. <a href=\"https:\/\/cloud.google.com\/maps-platform\/terms\">Google Maps terms<\/a>, <a href=\"https:\/\/policies.google.com\/privacy\">privacy<\/a>.<\/li>\n<li><strong>Cloudflare Turnstile<\/strong> (spam protection on the RSVP and booking forms): the visitor's browser loads Cloudflare's check, and your server sends Cloudflare the check's answer and the visitor's IP address. <a href=\"https:\/\/www.cloudflare.com\/website-terms\/\">Terms<\/a>, <a href=\"https:\/\/www.cloudflare.com\/turnstile-privacy-policy\/\">privacy<\/a>.<\/li>\n<li><strong>Akismet<\/strong> (spam protection): your server sends the name, email address, IP address and browser of the person who submits an RSVP or booking. <a href=\"https:\/\/akismet.com\/tos\/\">Terms<\/a>, <a href=\"https:\/\/automattic.com\/privacy\/\">privacy<\/a>.<\/li>\n<li><strong>Add to Calendar links<\/strong> (Google Calendar, Outlook.com, Yahoo Calendar): nothing is sent until a visitor clicks a link, which opens that service with the event's details. <a href=\"https:\/\/policies.google.com\/terms\">Google<\/a>, <a href=\"https:\/\/www.microsoft.com\/servicesagreement\">Microsoft<\/a>, <a href=\"https:\/\/legal.yahoo.com\/us\/en\/yahoo\/terms\/otos\/index.html\">Yahoo<\/a>.<\/li>\n<\/ul>\n\n<h3>Credits<\/h3>\n\n<ul>\n<li><a href=\"https:\/\/erikflowers.github.io\/weather-icons\/\">Weather Icons<\/a> by Erik Flowers (SIL Open Font License 1.1 and MIT).<\/li>\n<li><a href=\"https:\/\/sabre.io\/\">sabre\/vobject<\/a>, sabre\/xml and sabre\/uri by fruux GmbH (BSD 3-Clause).<\/li>\n<\/ul>\n\n<p>Other product names and logos shown in the plugin belong to their owners.<\/p>\n\n<h3>Privacy<\/h3>\n\n<p>HappenBoard stores attendee and booking data (name, email, RSVP or booking status) in your own WordPress database. It collects no usage data, needs no account, and contacts no outside service on a fresh install. The services it can use once you turn them on, and what each one receives, are listed under <strong>External services<\/strong> above.<\/p>\n\n<p>HappenBoard works with the WordPress privacy tools. A request made under <strong>Tools \u2192 Export Personal Data<\/strong> or <strong>Tools \u2192 Erase Personal Data<\/strong> covers the person's RSVPs and tickets, their bookings (with the notes staff added), their saved events and the AI agent log entries that name them. Erasing anonymizes RSVPs and bookings without deleting them, so attendance counts and the booking calendar stay right.<\/p>\n\n<!--section=installation-->\n<h4>From WordPress.org<\/h4>\n\n<ol>\n<li>In your WordPress admin, go to <strong>Plugins \u2192 Add New<\/strong> and search for <strong>HappenBoard<\/strong>.<\/li>\n<li>Click <strong>Install Now<\/strong>, then <strong>Activate<\/strong>.<\/li>\n<li>Follow the setup wizard: calendar defaults, notifications, and an optional import from another plugin.<\/li>\n<li>Go to <strong>HappenBoard \u2192 Events<\/strong> and click <strong>New event<\/strong>.<\/li>\n<li>Add the <strong>Calendar<\/strong> or <strong>Upcoming Events<\/strong> block to a page or a template.<\/li>\n<\/ol>\n\n<h4>Manual upload<\/h4>\n\n<ol>\n<li>Download the .zip from WordPress.org.<\/li>\n<li>Go to <strong>Plugins \u2192 Add New \u2192 Upload Plugin<\/strong>, choose the .zip and click <strong>Install Now<\/strong>.<\/li>\n<li>Activate the plugin and follow the setup wizard.<\/li>\n<\/ol>\n\n<h4>Requirements<\/h4>\n\n<ul>\n<li>WordPress 6.9 or newer<\/li>\n<li>PHP 8.2 or newer<\/li>\n<li>MySQL 5.7 or newer, or MariaDB 10.3 or newer<\/li>\n<\/ul>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20this%20work%20with%20classic%20themes%3F\"><h3>Does this work with classic themes?<\/h3><\/dt>\n<dd><p>Yes. You can add the blocks to any post, page or widget area. On a classic theme, an event page shows its date, countdown, RSVP form, venue, organizer, Add to Calendar links and categories around the event's description, and a booking type's page shows its booking form. The events archive shows the calendar and the upcoming events, and a category or tag archive shows its events, between your theme's header and footer. If your theme has its own <code>archive-hboard_event.php<\/code> or <code>taxonomy-happenboard_category.php<\/code>, HappenBoard uses it.<\/p>\n\n<p>On a block theme, HappenBoard adds block templates for event pages and archives, which you can edit in the Site Editor.<\/p><\/dd>\n<dt id=\"will%20this%20conflict%20with%20other%20event%20plugins%3F\"><h3>Will this conflict with other event plugins?<\/h3><\/dt>\n<dd><p>No. HappenBoard uses its own post type (<code>hboard_event<\/code>), meta keys (<code>_happenboard_*<\/code>), database tables (<code>happenboard_occurrences<\/code>, <code>happenboard_attendees<\/code>, <code>happenboard_bookings<\/code>, <code>happenboard_availability_rules<\/code>, <code>happenboard_agent_log<\/code>) and prefixed libraries (<code>HappenBoard\\Vendor\\\u2026<\/code>), so it can run next to another event plugin.<\/p>\n\n<p>The only thing they may share is the address. Events live under <code>\/events\/<\/code> by default, as in The Events Calendar, Events Manager and Sugar Calendar. You can change it under <strong>Settings \u2192 Permalinks \u2192 Event base<\/strong> (or with the <code>happenboard_event_rewrite_slug<\/code> filter). <strong>Tools \u2192 Site Health<\/strong> tells you when a page, a post type or a taxonomy uses the same address.<\/p><\/dd>\n<dt id=\"does%20happenboard%20add%20event%20schema%20%28json-ld%29%3F\"><h3>Does HappenBoard add Event schema (JSON-LD)?<\/h3><\/dt>\n<dd><p>Yes, on every event page, following <a href=\"https:\/\/developers.google.com\/search\/docs\/appearance\/structured-data\/event\">Google's Event structured data documentation<\/a>. If <a href=\"https:\/\/www.seopress.org\/\">SEOPress<\/a> is active and you set up an Event schema for the same post in SEOPress, HappenBoard leaves it to SEOPress so the page does not get two.<\/p><\/dd>\n<dt id=\"does%20it%20support%20recurring%20events%3F\"><h3>Does it support recurring events?<\/h3><\/dt>\n<dd><p>Yes. A repeat rule (every day, week, month or year, with exceptions) is saved in the standard iCalendar format and turned into a list of dates. Each date has its own page and its own RSVPs, and a date can be skipped. Moving one date of a series needs HappenBoard Pro.<\/p><\/dd>\n<dt id=\"where%20do%20i%20manage%20attendees%3F\"><h3>Where do I manage attendees?<\/h3><\/dt>\n<dd><p>Under <strong>HappenBoard \u2192 Attendees<\/strong>. You can search, filter by event or date, change a status or a number of seats, move an attendee to another date and export the list to CSV.<\/p><\/dd>\n<dt id=\"is%20happenboard%20translatable%3F\"><h3>Is HappenBoard translatable?<\/h3><\/dt>\n<dd><p>Yes. The text domain is <code>happenboard<\/code>, and translations are made on translate.wordpress.org. Developers can generate a fresh <code>.pot<\/code> file with <code>npm run makepot<\/code>.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20wpml%20or%20polylang%3F\"><h3>Does it work with WPML or Polylang?<\/h3><\/dt>\n<dd><p>Yes. Events, venues, organizers and their categories and tags can be translated, through the bundled <code>wpml-config.xml<\/code>. The calendar, lists, grids and Query Loop show the events of the current language, and a translated event shows its venue and organizer in the same language. RSVP emails are sent in the language of the event the attendee registered on, and booking emails in the language of the page the booking was made on. The site's iCal feed lists the default language's events; add <code>?lang=fr<\/code> (Polylang) to its address for another language.<\/p>\n\n<p>The translations of an event share its seats. Registrations on any translation count against one capacity, the one set on the default language's event (per date, on a recurring event). Every translation shows the same seats remaining, and an email address registered on one translation is registered on all of them. In the admin, an event's attendee list shows the attendees of all its translations, with a Language column; untick \"All languages\" to see one translation.<\/p>\n\n<p>Booking types are not translated: one booking type serves every language, so its availability and bookings stay in one place. Its title and description show as you wrote them, and the rest of the booking page is translated.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20page%20caching%2C%20a%20cdn%20or%20a%20reverse%20proxy%3F\"><h3>Does it work with page caching, a CDN or a reverse proxy?<\/h3><\/dt>\n<dd><p>Yes. The RSVP and booking forms contain nothing personal or time-limited, so a page cache can serve them to every visitor. Each form fetches its anti-spam timestamp when the visitor starts filling it in. For a signed-in visitor the form contains a security token, so HappenBoard asks page caches not to store that page.<\/p>\n\n<p>Behind a reverse proxy or a CDN, every request reaches WordPress from the proxy's address, so the per-address limits on RSVPs and bookings would treat all your visitors as one. Give HappenBoard the visitor's real address with the <code>happenboard_rsvp_client_ip<\/code> and <code>happenboard_booking_client_ip<\/code> filters. Behind Cloudflare, for example:<\/p>\n\n<pre><code>add_filter( 'happenboard_rsvp_client_ip', fn( $ip ) =&gt; filter_var( $_SERVER['HTTP_CF_CONNECTING_IP'] ?? '', FILTER_VALIDATE_IP ) ?: $ip );\n<\/code><\/pre>\n\n<p>Add the same line for <code>happenboard_booking_client_ip<\/code>. Only trust a header that your proxy sets: if the site can also be reached directly, anyone can send that header.<\/p><\/dd>\n<dt id=\"how%20do%20i%20uninstall%20happenboard%20and%20erase%20all%20its%20data%3F\"><h3>How do I uninstall HappenBoard and erase all its data?<\/h3><\/dt>\n<dd><p>Deleting the plugin from <strong>Plugins \u2192 Installed Plugins<\/strong> keeps your data: events, attendees, settings, categories and tags stay in the database, so installing HappenBoard again brings everything back.<\/p>\n\n<p>To erase everything, add this line to your <code>wp-config.php<\/code> before you click <strong>Delete<\/strong>:<\/p>\n\n<pre><code>define( 'HAPPENBOARD_UNINSTALL', true );\n<\/code><\/pre>\n\n<p>Deleting the plugin then:<\/p>\n\n<ul>\n<li>removes every event, venue, organizer and booking type, with their data;<\/li>\n<li>removes every event category and tag;<\/li>\n<li>drops the tables <code>wp_happenboard_occurrences<\/code>, <code>wp_happenboard_attendees<\/code>, <code>wp_happenboard_bookings<\/code>, <code>wp_happenboard_availability_rules<\/code> and <code>wp_happenboard_agent_log<\/code>;<\/li>\n<li>deletes the options and transients starting with <code>happenboard_<\/code>, except HappenBoard Pro's;<\/li>\n<li>cancels the plugin's scheduled tasks, including every pending reminder;<\/li>\n<li>deletes the plugin's per-user data (saved events, dashboard layout, dismissed guides);<\/li>\n<li>removes HappenBoard's capabilities from every role and user.<\/li>\n<\/ul>\n\n<p>On a multisite network this runs on every site. Afterwards you can remove the line from <code>wp-config.php<\/code>.<\/p>\n\n<p>HappenBoard Pro's data (its settings, payment keys, licence, tables and capabilities) is not part of this. Pro erases it when you delete Pro with <code>define( 'HAPPENBOARD_PRO_UNINSTALL', true );<\/code> in <code>wp-config.php<\/code>. To erase everything, add both lines and delete both plugins.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>IMPORTANT: Events, venues and organizers now have their own permissions. Administrators and editors keep full access. Authors and Contributors no longer see them; a role editor can give them back with the <code>happenboard_manage_events<\/code>, <code>happenboard_manage_venues<\/code> and <code>happenboard_manage_organizers<\/code> capabilities.<\/li>\n<li>NEW: Event form: add a category, tag, venue or organizer without leaving the form, and press Enter to save. A new category or tag is only created when the event is saved.<\/li>\n<li>NEW: RSVP form: a signed-in visitor finds their name and email filled in. An RSVP goes to the visitor's account (and their My Schedule) only when its email is the account's. The attendee panel shows the event, the status, the seats and the RSVP date.<\/li>\n<li>NEW: iCal feed: the feed carries your site's name, keeps the last 30 days, and is announced on the events archive so calendar apps find it. The Calendar block can show a \"Subscribe to this calendar\" link.<\/li>\n<li>NEW: Event Query Loop and Events List: \"One entry per event\" shows a series once. The Query Loop takes an offset, and the Events List can end with a \"More events\" link. Venue and organizer pages list each upcoming event once, with pages.<\/li>\n<li>NEW: Admin Calendar: filter by Recurring and by date range. The Events list filters by tag, venue and organizer too, and Venues, Organizers and Booking types show counts on their tabs.<\/li>\n<li>NEW: Settings: Save sits in the page header and is active only when something changed.<\/li>\n<li>IMPROVEMENT: Every admin screen uses the site's date format and the WordPress date picker, which starts on the site's first day of the week. All-day events show their date alone.<\/li>\n<li>IMPROVEMENT: The setup wizard opens once after activation, the browser's Back button moves between its steps, and it looks like the rest of the admin.<\/li>\n<li>IMPROVEMENT: Attendees, Categories and Tags say what to do next when they are empty, and each admin screen has its own browser-tab title.<\/li>\n<li>IMPROVEMENT: Attendees get the online meeting link in their emails and calendar files. A password-protected event keeps the link behind its password.<\/li>\n<li>IMPROVEMENT: Search results and feeds show an event's date instead of the date it was published.<\/li>\n<li>IMPROVEMENT: Search engines no longer crawl endless calendar months: calendar navigation is not indexed, and months far from any event answer with a 404.<\/li>\n<li>FIX: Calendar block: readable event titles, a message when a month or week has no events, short events in the week view, the month view on phones, and the admin color for events without a category. All-day events stay on their dates when the site's time zone changes, and cancelled or moved dates of a series are marked.<\/li>\n<li>FIX: Blocks: cancelled and postponed events are labelled everywhere, ended and in-progress events are handled the same way in every block, event titles get the right heading level, and the venue, organizer and RSVP lines of an event are tidied. Venue photos and organizer logos show on their pages. An event block placed outside an event explains why it is empty.<\/li>\n<li>FIX: RSVPs: the form follows its own seat limit and refreshes when the event is full, the per-email limit counts per event and after the captcha check, and an attendee's email cannot be changed to one already registered. Without JavaScript, the name and email no longer end up in the address bar. Deleting an event for good emails its attendees.<\/li>\n<li>FIX: RSVPs keep working when the bundled libraries are missing from an install, and administrators are told how to restore them.<\/li>\n<li>FIX: Bookings: a booking type with upcoming bookings cannot be deleted, moving a booking to its own time sends no email, and the booking blocks show their errors in your language. Availability shows refused hours on their row and warns before leaving unsaved hours.<\/li>\n<li>FIX: The RSVP and booking link pages are not indexed, have their own title, and answer with a 404 for an unknown link.<\/li>\n<li>FIX: Admin: errors show on their fields, the end moves with the start, and an end picked before the start moves to the afternoon. Trashing events refreshes the counts and offers Undo. A new venue or organizer stays in view, the form stays visible under the discard question, notices no longer cover the menu, settings refuse an invalid value on its field instead of saying \"Saved\", and the admin Calendar filtered by a category shows all its events, not the first 100.<\/li>\n<li>FIX: Editor: the event dates in the When panel wrap instead of running under Clear.<\/li>\n<li>FIX: Event schema: closed ticket offers are left out, and an in-person event is no longer marked as online. The organizer's email is no longer published.<\/li>\n<li>FIX: Imports and migration: imported titles and content are cleaned, backslashes are kept, duplicated events lose the import markers, an .ics repeating series keeps its time zone, and the report of a real migration stays on screen.<\/li>\n<li>FIX: AI agents: event dates are checked, and a duplicated event gets its own dates.<\/li>\n<li>FIX: REST API: negative, out-of-range and unknown values are refused, meeting links and venue and organizer websites are checked, and missing items answer with a 404.<\/li>\n<li>FIX: Typos and unclear wording in the admin, the recurrence editor and the exports.<\/li>\n<li>SECURITY: Cancellation links are stored as a hash and cleared when personal data is erased. Attendee emails are shown only to users allowed to export attendees, and public booking answers no longer reveal host user IDs.<\/li>\n<li>PERFORMANCE: Each admin screen loads only when you open it.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>IMPORTANT: This release updates the database on the first page load after the update. Recurring events are rebuilt in your site's time zone, the exceptions you saved move with them, and existing RSVPs on recurring events are given the date they were made for. Back up your site before updating.<\/li>\n<li>IMPORTANT: HappenBoard Pro 1.0.0 requires this version.<\/li>\n<li>NEW: An RSVP on a recurring event is for one date, and the event's capacity applies to each date. The Attendees screen shows, filters and exports each attendee's date, and can move an attendee to another date, emailing them the new date with a calendar file.<\/li>\n<li>NEW: The RSVP form offers every date of a long series, 60 at a time, and says when a date is full.<\/li>\n<li>NEW: Booking page: customers can choose how long to book and, when the booking type lets them, their host. The price is quoted live as they choose, deposit included. A signed-in visitor finds their name and email filled in.<\/li>\n<li>NEW: Availability: write the whole rule stack (date ranges, seasons, weeks, weekdays, closures), let a booking type be open by default, set the time of the first slot and the weekdays a booking can start on.<\/li>\n<li>NEW: Bookings can be shown as a layer on the admin Calendar.<\/li>\n<li>NEW: A declined booking request gets its own \"request declined\" email instead of a cancellation, and its own Declined status: a tab on the Bookings screen, \"Request declined\" in My Bookings and on the booking link, which no longer offers to cancel it. Requests declined before this version stay listed as cancelled.<\/li>\n<li>NEW: The event address base (<code>events<\/code> by default) can be changed under Settings \u2192 Permalinks. Pages under the base are served, and conflicts are reported in Site Health.<\/li>\n<li>NEW: Classic themes get the full single-event page, and the event archives show the same blocks as the block templates.<\/li>\n<li>NEW: Customers can move a booking to another time from the link in their booking emails. A moved booking is emailed with an updated calendar invite, a cancelled one is taken out of the customer's calendar, and hosts and the site admin are told about new, moved and cancelled bookings.<\/li>\n<li>NEW: Attendees are emailed when the date they registered for is cancelled or moved, and when their RSVP is cancelled. RSVP emails say where the event takes place and which time zone its time is in, and the reminder carries the cancellation link.<\/li>\n<li>NEW: Venue and organizer pages show the address, contact details and upcoming events, with Site Editor templates of their own. Event tags get an archive template like categories.<\/li>\n<li>NEW: Event Query Loop: \"Today\", \"This week\" and custom date ranges, and a <code>search<\/code> attribute (set in the code editor, or by HappenBoard Pro's Filter Bar).<\/li>\n<li>NEW: Multilingual sites: RSVP emails are sent in the language of the event the attendee registered on, booking emails in the language of the page the booking was made on, and the site's iCal feed lists one language's events.<\/li>\n<li>NEW: Multilingual sites: an event's translations share its seats. Registrations on every translation count against the default language's capacity, each page shows the same seats remaining, one email registers once whatever the language, and the admin's attendee list and export of an event show every translation's attendees with their language.<\/li>\n<li>NEW: AI agents: HappenBoard has its own MCP server. Paste its address into Claude, ChatGPT, Cursor or any MCP client, sign in with your WordPress account, and choose whether the connection may only read or may also make changes. Settings \u2192 AI agents shows the steps for each client, the connected clients with a Disconnect button, and a setup check. AI clients can now create and update events, bookings, RSVPs, venues and organizers; deleting, and cancelling a booking, stay out of their reach. A connection that may make changes can set an attendee's status, cancelled included, as the Attendees screen does.<\/li>\n<li>NEW: Extension points used by HappenBoard Pro: person types on bookings, the price stored on each booking, online deposits and card holds for requests that need approval, a filter to refuse free RSVPs on an event, a filter that marks an event as ticketed, and undone check-ins and wrong-event refusals in the attendee timeline.<\/li>\n<li>NEW: A new setup wizard: one step per screen, a live preview of your first event and of the emails your attendees receive, and on the last screen an optional newsletter sign-up (nothing is sent before you click Subscribe).<\/li>\n<li>NEW: Venues screen: each venue with its photo and address, a grid view, filters by city, country and \"Not on the map\", and a panel with the venue's details.<\/li>\n<li>NEW: Calendar: click the month or date in the heading to jump to another month or day.<\/li>\n<li>NEW: The event editor's guide has a picture on each page, and the Dashboard opens on a new Welcome banner.<\/li>\n<li>IMPROVEMENT: The WordPress admin bar stays visible on HappenBoard screens, the status tabs use the WordPress design system, and the HappenBoard menu has its own icon, just below Pages.<\/li>\n<li>IMPROVEMENT: HappenBoard is now licensed under GPLv3 or later.<\/li>\n<li>IMPROVEMENT: This readme lists every external service HappenBoard can use and what each one receives.<\/li>\n<li>FIX: Recurring events: rules are expanded in the site's time zone (series no longer shift by an hour across daylight saving time or land on the wrong weekday), long series stay current, BYMONTH rules work, events spanning several days show on each day, all-day events export as dates, and an end before the start is refused. Rules the plugin cannot expand are refused when saved, the rule builder keeps the parts of a rule it has no control for and offers monthly by weekday or by day of the month, and the series summary names the days.<\/li>\n<li>FIX: Front end: the RSVP and booking forms work behind page caches and submit only once; single-event blocks respect post passwords and read access; grids and lists show their empty state when a filter matches nothing; past Query Loops list newest first; the \"All dates\" page and the cancellation pages render with the theme's header and footer. The linked venue and organizer are named on every surface (lists, grids, Query Loops, Add to Calendar, My Schedule, emails, Event schema); Add to Calendar keeps the description's line breaks; category and tag archives paginate; password-protected events lose the \"Protected:\" prefix in emails, feeds and calendars; an event without a start cannot be published or take RSVPs; an RSVP stays within its form's seat limit; a Query Loop saved without a layout shows its list; an organizer's email address is printed protected from spam bots.<\/li>\n<li>FIX: Calendar block: each date of a series links to that date, the period is kept when switching views, past days and events are marked, and a period with more events than it can show says so.<\/li>\n<li>FIX: Translations: front-end strings, plurals, dates and block defaults are translatable and script translations load. WPML and Polylang get the plugin's real data model, and public lists stay in the current language. Calendar times follow the site's time format, dates and times are joined and time zones named in a translatable way (\"Paris time, UTC+2\"), arrows turn in right-to-left languages, plurals and words with several meanings have their own strings, and the admin lists' built-in strings are translated. <code>wpml-config.xml<\/code> covers every field and translatable block label.<\/li>\n<li>FIX: Admin: the Calendar follows the site's time zone and dragging one date of a series moves only that date; attendee and booking search work; activity and booking times no longer shift by the browser's time zone; the Events date filter keeps the site's day. The admin lists no longer break when the Gutenberg plugin is active, and a screen that fails offers a reload; the Calendar shows an event lasting several days on each of them and keeps its period across views; restoring from the Trash works as in WordPress; a new event's end takes the default event length; the booking-type editor has the daily cap and the Availability screen each type's own hours, with weekly hours validated; venue coordinates and websites are checked, and an attendee edit stays within the capacity; the dashboard's setup checklist ticks \"Connect a payment gateway\" only when payments are configured; a link to an integration opens its card; an agent's write refused for lack of permission is recorded in the agent log.<\/li>\n<li>FIX: Bookings: a booking type that needs approval asks instead of confirming, a booking is only moved to a time a host is free (its own host, or another free host on a round-robin type), emails have a body and a cancellation link, the calendar invite is standards-compliant, and slot lists refresh when a booking type changes. A host can no longer be double-booked across booking types or within another booking's buffers.<\/li>\n<li>FIX: Accessibility: the Calendar block has enough contrast, keeps the focus, works with the keyboard and fits a phone; the booking calendars keep the focus and say the month; form errors are tied to their fields and announced, busy buttons say so, and confirmations take the focus; Save, Remove, View, Add to Calendar and Configure links and buttons, the event pickers, the admin's dialogs and drag handles are named; heading levels follow the page. In the admin, the focus moves to each new screen and comes back from the command palette and the attendee panel, and the app has a layout for narrow screens.<\/li>\n<li>FIX: Notifications: a cancellation is sent once, and reminders follow the real date and are sent once.<\/li>\n<li>FIX: The iCal feed is served as a calendar, and the .ics button has its own route. A password-protected event's own .ics download works and lists it as the site feed does, without its description.<\/li>\n<li>FIX: Importers: categories are read from the source's term tables, ICS dates are read in their own time zone, imported series end on the right day, and EventKoi and Amelia imports get their dates. The Events Calendar's all-day events, EventKoi Lite's RSVPs and a re-import after trashing an event are imported; an .ics series keeps its moved and cancelled dates, and an empty or unreadable file is reported as an error.<\/li>\n<li>FIX: CSV: the events CSV keeps categories and online details, and the attendee export has plain-text titles and its dates.<\/li>\n<li>FIX: The Attendees screen offers the exports, and HappenBoard Pro's badges, only to users allowed to export attendees (administrators by default). Editors saw them and were refused.<\/li>\n<li>FIX: Event schema: a ticketed event is no longer published as free, and one that is full is sold out.<\/li>\n<li>FIX: Deleting an event for good also deletes its attendees, their reminders and the rows add-ons keep for it.<\/li>\n<li>FIX: Privacy: the personal-data exporter and eraser also cover bookings, saved events and agent-log entries.<\/li>\n<li>FIX: Multisite: network activation sets up every site, and each database migration is verified.<\/li>\n<li>FIX: A full uninstall also removes scheduled reminders, user data and capabilities granted to single users, the attendee-management capability included; deactivation removes HappenBoard's addresses and its recurring background tasks.<\/li>\n<li>FIX: Each currency uses its own number of decimals (none for the yen, for example).<\/li>\n<li>FIX: Elementor widgets render when saved on a page and are translatable.<\/li>\n<li>FIX: Event and booking addresses that stopped working because another plugin rebuilt the rewrite rules are repaired automatically.<\/li>\n<li>FIX: CSV import: imported events get their dates at once (they were missing from the admin Calendar and the calendar and list blocks until saved again), the apostrophe the export puts before a formula-like cell is removed again, and the Tools card lists all 19 columns, which may come in any order.<\/li>\n<li>FIX: Migration screen: a source of any size is migrated to the end (the run stopped after 10,000 events and was shown as finished), and a run that stops early says so and can continue.<\/li>\n<li>FIX: Migration: a series from The Events Calendar or Events Manager no longer shows the single dates it excluded: they come in as cancelled dates. An event whose extra repeat rules or repeating exclusions (\"every Monday\") were left out, or whose repeat rule could not be converted, is listed under the run's warnings, in the Migration screen and in WP-CLI. It used to come in without a word. EventKoi venues are counted once, when they are created, not once per event held there.<\/li>\n<li>FIX: Migration: the dates of an Events Manager series that last several days (Friday 10:00 to Saturday 16:00) keep their length; each one ended on its start day. Events Manager's repeating events, whose dates have pages of their own, come in as a series with their excluded dates and warnings, like its recurring events. They came in as one event running from the first date to the last.<\/li>\n<li>FIX: RSVPs: two visitors registering for the last seat at the same moment can no longer both get it, and neither can a visitor and an operator using \"Mark as Going\", a seat change or \"Change date\" on the Attendees screen.<\/li>\n<li>FIX: Migration: Events Manager bookings on the dates of a series are imported on those dates (they were dropped), and Events Manager booking statuses are read correctly (approved bookings came in on the waitlist, pending and unpaid ones as Going). The Events Calendar's PayPal and Tickets Commerce attendees are imported with their order status, and its \"Not going\" RSVPs as declined. Running a migration again no longer copies cancelled bookings and declined RSVPs a second time.<\/li>\n<li>FIX: AI agents: the booking-type abilities offer round-robin host assignment as <code>round_robin<\/code>. The <code>least_loaded<\/code> value they offered was stored as is and gave every booking to the first free host; it is now stored and read as round-robin.<\/li>\n<li>FIX: Attendees: \"Mark as Going\", the attendee panel's status change and the update-attendee-status ability check the capacity, with the same \"over capacity\" override as a seat change.<\/li>\n<li>FIX: Uninstall: a full wipe of the free plugin no longer deletes HappenBoard Pro's options (settings, payment keys, licence) or the meta Pro keeps on WooCommerce products and orders; Pro's data is removed by Pro's own uninstall. The option sweep matches its prefix literally.<\/li>\n<li>FIX: Admin: Editors are no longer stuck on the setup wizard and are not offered Settings shortcuts; the Docs link opens the documentation; the Events list names the linked venue; \"Custom iframe URL\" can be saved as the default map provider.<\/li>\n<li>FIX: Event schema: a password-protected event's markup has no description (it read \"There is no excerpt because this is a protected post.\"). Organizer pages print the email address protected from spam bots.<\/li>\n<li>FIX: Bookings: a booking that took place can be marked as completed or as a no-show from the Bookings screen (the two tabs had no way in); a booking past midnight shows on both days of the admin Calendar; the booking type editor takes durations of 1 to 4 hours in hours and has a Description field.<\/li>\n<li>FIX: Admin: an administrator keeps every HappenBoard screen in the menu even when a role editor took a HappenBoard permission away, as the routes allow; the quick form fills the end from the start; Default event duration goes from 5 to 1,440 minutes; a day clicked in the Calendar's Month view starts at 09:00; categories and tags ask before they are deleted; a venue or organizer restored from the Trash is published again, and their Trash offers Delete permanently; an invalid organizer email or out-of-range coordinates are refused in plain words; a website typed without https:\/\/ gets it.<\/li>\n<li>FIX: Blocks: Events Grid and Event Query Loop print the day of an all-day date; the listing blocks link each date of a series to that date; an unfiltered Events Grid keeps a date under way; the RSVP Form shows its own Success message; Event Date &amp; Time, Venue, Organizer, Series and Weather check the visitor may read the event; the Event Weather block drops a unit setting that did nothing, and Save Event Button an option that did nothing.<\/li>\n<li>FIX: Emails: the button follows the Header accent color, and the footer keeps its links and emphasis.<\/li>\n<li>FIX: Event schema: a cancelled event synced from Google Calendar or Outlook is published as cancelled.<\/li>\n<li>FIX: Imports: CSV start_utc and end_utc are checked; an .ics event with a repeat rule HappenBoard cannot use is imported on its first date with a note, an event imported again without a rule loses the old one, and .ical files can be picked. WP-CLI refuses an unknown --status.<\/li>\n<li>FIX: Privacy: the agent log's export and eraser also cover the error message, and the export of a sign-up names the date it was for.<\/li>\n<li>FIX: Help texts and messages that described something the plugin does not do (sender identity, Event base, spam protection, meeting URL, health check, editor tour, the setup wizard's import step, the duplicate-RSVP message) were rewritten.<\/li>\n<li>FIX: Security hardening.<\/li>\n<li>PERFORMANCE: The site's iCal feed is cached and answers revalidation with a 304, occurrence lists load their posts in one query, failed weather lookups are remembered for five minutes, and the admin Events date filter reads the occurrences table. Lists read Polylang's language terms and each event's current date once, the saved-events lookup and the seats count are batched, block defaults are translated for HappenBoard's blocks only and the blocks register from one manifest, calendar links no longer go through a redirect, each event's .ics answers revalidation, Cloudflare Turnstile loads deferred, booking slot lists are cached only with a persistent object cache and an anonymous one can be reused for a minute, and HappenBoard Pro's Orders screen reads an index.<\/li>\n<\/ul>","raw_excerpt":"Events calendar and appointment booking for the block editor and the Site Editor: recurring events, RSVPs, iCal feeds and Event schema.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/313469","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=313469"}],"author":[{"embeddable":true,"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/rainbowgeek"}],"wp:attachment":[{"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=313469"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=313469"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=313469"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=313469"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=313469"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/pe.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=313469"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}