From 4bb7173476bbdbacf8ed747477b43dfe1db6637a Mon Sep 17 00:00:00 2001 From: Laurence Horrocks-Barlow Date: Fri, 15 May 2026 17:25:41 +0100 Subject: [PATCH] Initial commit: Speakers for The Events Calendar v1.0.0 --- speakers-for-the-events-calendar.php | 572 +++++++++++++++++++++++++++ 1 file changed, 572 insertions(+) create mode 100644 speakers-for-the-events-calendar.php diff --git a/speakers-for-the-events-calendar.php b/speakers-for-the-events-calendar.php new file mode 100644 index 0000000..5695c61 --- /dev/null +++ b/speakers-for-the-events-calendar.php @@ -0,0 +1,572 @@ + [ + 'name' => __( 'Speakers', 'speakers-for-tec' ), + 'singular_name' => __( 'Speaker', 'speakers-for-tec' ), + 'add_new_item' => __( 'Add New Speaker', 'speakers-for-tec' ), + 'edit_item' => __( 'Edit Speaker', 'speakers-for-tec' ), + 'new_item' => __( 'New Speaker', 'speakers-for-tec' ), + 'view_item' => __( 'View Speaker', 'speakers-for-tec' ), + 'search_items' => __( 'Search Speakers', 'speakers-for-tec' ), + 'not_found' => __( 'No speakers found', 'speakers-for-tec' ), + 'not_found_in_trash' => __( 'No speakers in trash','speakers-for-tec' ), + 'all_items' => __( 'All Speakers', 'speakers-for-tec' ), + 'menu_name' => __( 'Speakers', 'speakers-for-tec' ), + ], + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'query_var' => true, + 'rewrite' => [ 'slug' => 'speakers' ], + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => 6, + 'menu_icon' => 'dashicons-groups', + 'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt' ], + 'show_in_rest' => true, + ] ); + } + + // ------------------------------------------------------------------------- + // Meta boxes + // ------------------------------------------------------------------------- + + public function add_meta_boxes(): void { + // Extra fields on speaker edit screen + add_meta_box( + 'sftec_speaker_details', + __( 'Speaker Details', 'speakers-for-tec' ), + [ $this, 'render_speaker_details_meta' ], + self::CPT, + 'side', + 'default' + ); + + // Speaker picker on TEC event edit screen + if ( post_type_exists( 'tribe_events' ) ) { + add_meta_box( + 'sftec_event_speakers', + __( 'Speakers', 'speakers-for-tec' ), + [ $this, 'render_event_speakers_meta' ], + 'tribe_events', + 'normal', + 'default' + ); + } + } + + public function render_speaker_details_meta( \WP_Post $post ): void { + wp_nonce_field( 'sftec_speaker_details_save', '_sftec_speaker_nonce' ); + $website = get_post_meta( $post->ID, '_tec_speaker_website', true ); + $email = get_post_meta( $post->ID, '_tec_speaker_email', true ); + ?> +

+
+ +

+

+
+ +

+

+ ID, self::META_KEY, true ) ?: [] ); + + $speakers = get_posts( [ + 'post_type' => self::CPT, + 'posts_per_page' => -1, + 'orderby' => 'title', + 'order' => 'ASC', + 'post_status' => 'publish', + ] ); + + if ( empty( $speakers ) ) { + printf( + '

%s %s

', + esc_html__( 'No speakers yet.', 'speakers-for-tec' ), + esc_url( admin_url( 'post-new.php?post_type=' . self::CPT ) ), + esc_html__( 'Add speakers first, then return here to attach them.', 'speakers-for-tec' ) + ); + return; + } + + echo ''; + echo '

' + . esc_html__( 'Type to search. Click a name to add, click again to remove.', 'speakers-for-tec' ) + . '

'; + } + + // ------------------------------------------------------------------------- + // Saving meta + // ------------------------------------------------------------------------- + + public function save_meta( int $post_id, \WP_Post $post ): void { + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + // Speaker detail fields + if ( + isset( $_POST['_sftec_speaker_nonce'] ) + && wp_verify_nonce( $_POST['_sftec_speaker_nonce'], 'sftec_speaker_details_save' ) + && $post->post_type === self::CPT + && current_user_can( 'edit_post', $post_id ) + ) { + update_post_meta( $post_id, '_tec_speaker_website', sanitize_url( $_POST['sftec_speaker_website'] ?? '' ) ); + update_post_meta( $post_id, '_tec_speaker_email', sanitize_email( $_POST['sftec_speaker_email'] ?? '' ) ); + } + + // Event → speaker associations + if ( + isset( $_POST['_sftec_event_speakers_nonce'] ) + && wp_verify_nonce( $_POST['_sftec_event_speakers_nonce'], 'sftec_event_speakers_save' ) + && $post->post_type === 'tribe_events' + && current_user_can( 'edit_post', $post_id ) + ) { + $ids = array_map( 'intval', (array) ( $_POST['sftec_event_speakers'] ?? [] ) ); + update_post_meta( $post_id, self::META_KEY, array_filter( $ids ) ); + } + } + + // ------------------------------------------------------------------------- + // Front-end: speakers on event pages + // ------------------------------------------------------------------------- + + public function display_event_speakers(): void { + $post_id = get_the_ID(); + $speaker_ids = (array) ( get_post_meta( $post_id, self::META_KEY, true ) ?: [] ); + + if ( empty( $speaker_ids ) ) { + return; + } + + $speakers = get_posts( [ + 'post_type' => self::CPT, + 'post__in' => $speaker_ids, + 'posts_per_page' => -1, + 'orderby' => 'post__in', + 'post_status' => 'publish', + ] ); + + if ( empty( $speakers ) ) { + return; + } + + $this->enqueue_styles(); + + $heading = count( $speakers ) > 1 + ? __( 'Speakers', 'speakers-for-tec' ) + : __( 'Speaker', 'speakers-for-tec' ); + + echo '
'; + echo '

' . esc_html( $heading ) . '

'; + echo '
'; + + foreach ( $speakers as $speaker ) { + $this->render_speaker_card( $speaker, 'event' ); + } + + echo '
'; + } + + // ------------------------------------------------------------------------- + // Shortcode: [tec_speakers limit="20" search=""] + // ------------------------------------------------------------------------- + + public function speakers_shortcode( array $atts ): string { + $atts = shortcode_atts( [ 'limit' => 50, 'search' => '' ], $atts, 'tec_speakers' ); + + $args = [ + 'post_type' => self::CPT, + 'posts_per_page' => (int) $atts['limit'], + 'orderby' => 'title', + 'order' => 'ASC', + 'post_status' => 'publish', + ]; + if ( $atts['search'] ) { + $args['s'] = sanitize_text_field( $atts['search'] ); + } + + $speakers = get_posts( $args ); + if ( empty( $speakers ) ) { + return '

' . esc_html__( 'No speakers found.', 'speakers-for-tec' ) . '

'; + } + + $this->enqueue_styles(); + + ob_start(); + echo '
'; + foreach ( $speakers as $speaker ) { + $this->render_speaker_card( $speaker, 'archive' ); + } + echo '
'; + + return ob_get_clean(); + } + + // ------------------------------------------------------------------------- + // Shared card renderer + // ------------------------------------------------------------------------- + + private function render_speaker_card( \WP_Post $speaker, string $context = 'event' ): void { + $permalink = get_permalink( $speaker->ID ); + $website = get_post_meta( $speaker->ID, '_tec_speaker_website', true ); + + echo '
'; + + if ( has_post_thumbnail( $speaker->ID ) ) { + echo ''; + } + + echo '
'; + echo '

' . esc_html( $speaker->post_title ) . '

'; + + $excerpt = $speaker->post_excerpt ?: wp_trim_words( wp_strip_all_tags( $speaker->post_content ), 30 ); + if ( $excerpt ) { + echo '

' . esc_html( $excerpt ) . '

'; + } + + if ( $website ) { + echo '' + . esc_html__( 'Website', 'speakers-for-tec' ) . ' →'; + } + + echo '
'; + echo '
'; + } + + // ------------------------------------------------------------------------- + // Styles (enqueued once per page load) + // ------------------------------------------------------------------------- + + private function enqueue_styles(): void { + static $done = false; + if ( $done ) { + return; + } + $done = true; + + echo ''; + } + + // ------------------------------------------------------------------------- + // Admin: Select2 on event edit screens + // ------------------------------------------------------------------------- + + public function admin_enqueue_scripts( string $hook ): void { + $screen = get_current_screen(); + if ( ! $screen || $screen->post_type !== 'tribe_events' || ! in_array( $hook, [ 'post.php', 'post-new.php' ], true ) ) { + return; + } + + // Use WP-bundled Select2 if available (TEC ships it), otherwise CDN fallback + if ( wp_script_is( 'select2', 'registered' ) ) { + wp_enqueue_script( 'select2' ); + wp_enqueue_style( 'select2' ); + $handle = 'select2'; + } else { + wp_enqueue_script( + 'sftec-select2', + 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js', + [ 'jquery' ], + '4.1.0', + true + ); + wp_enqueue_style( + 'sftec-select2', + 'https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css', + [], + '4.1.0' + ); + $handle = 'sftec-select2'; + } + + wp_add_inline_script( + $handle, + "jQuery(function($){ + $('#sftec-speaker-select').select2({ + placeholder: '" . esc_js( __( 'Search for a speaker…', 'speakers-for-tec' ) ) . "', + allowClear: true, + width: '100%' + }); + });" + ); + } + + // ------------------------------------------------------------------------- + // Admin: CSV import page + // ------------------------------------------------------------------------- + + public function add_admin_menu(): void { + add_submenu_page( + 'edit.php?post_type=' . self::CPT, + __( 'Import Speakers from CSV', 'speakers-for-tec' ), + __( 'Import CSV', 'speakers-for-tec' ), + 'manage_options', + 'sftec-import-speakers', + [ $this, 'render_import_page' ] + ); + } + + public function render_import_page(): void { + ?> +
+

+ +

+ name, bio, email, website, image + + + +

+ +
+ + + + + + + + + + + + +
+ +
+ + +
+
+ import_redirect( [ 'sftec_err' => 'no_file' ] ) ); + exit; + } + + $handle = fopen( $_FILES['speakers_csv']['tmp_name'], 'r' ); + if ( ! $handle ) { + wp_redirect( $this->import_redirect( [ 'sftec_err' => 'read_fail' ] ) ); + exit; + } + + $skip_header = ! empty( $_POST['skip_header'] ); + + // Build name index to prevent duplicates + $existing_ids = get_posts( [ + 'post_type' => self::CPT, + 'posts_per_page' => -1, + 'fields' => 'ids', + 'post_status' => 'any', + ] ); + $existing_names = []; + foreach ( $existing_ids as $id ) { + $existing_names[ $this->normalise_name( get_the_title( $id ) ) ] = true; + } + + $created = $skipped = $errors = 0; + $row_num = 0; + + while ( ( $row = fgetcsv( $handle ) ) !== false ) { + $row_num++; + if ( $skip_header && $row_num === 1 ) { + continue; + } + if ( empty( array_filter( $row ) ) ) { + continue; + } + + [ $name, $bio, $email, $website ] = array_pad( $row, 4, '' ); + $name = trim( $name ); + + if ( ! $name ) { + continue; + } + + $key = $this->normalise_name( $name ); + if ( isset( $existing_names[ $key ] ) ) { + $skipped++; + continue; + } + + $post_id = wp_insert_post( [ + 'post_type' => self::CPT, + 'post_title' => sanitize_text_field( $name ), + 'post_content' => wp_kses_post( $bio ), + 'post_excerpt' => wp_trim_words( wp_strip_all_tags( $bio ), 30 ), + 'post_status' => 'publish', + ] ); + + if ( is_wp_error( $post_id ) ) { + $errors++; + continue; + } + + if ( $email ) { + update_post_meta( $post_id, '_tec_speaker_email', sanitize_email( trim( $email ) ) ); + } + if ( $website ) { + update_post_meta( $post_id, '_tec_speaker_website', sanitize_url( trim( $website ) ) ); + } + + $existing_names[ $key ] = true; + $created++; + } + + fclose( $handle ); + + wp_redirect( $this->import_redirect( [ + 'sftec_created' => $created, + 'sftec_skipped' => $skipped, + 'sftec_errors' => $errors, + ] ) ); + exit; + } + + private function import_redirect( array $args ): string { + return add_query_arg( + array_merge( [ 'post_type' => self::CPT, 'page' => 'sftec-import-speakers' ], $args ), + admin_url( 'edit.php' ) + ); + } + + private function normalise_name( string $name ): string { + $name = strtolower( trim( $name ) ); + $name = preg_replace( '/^(dr\.?|prof\.?|mr\.?|mrs\.?|ms\.?|miss\.?)\s+/u', '', $name ); + return preg_replace( '/\s+/', ' ', $name ); + } + + // ------------------------------------------------------------------------- + // Admin notices: import result banner + // ------------------------------------------------------------------------- + + public function admin_notices(): void { + if ( ! isset( $_GET['sftec_created'] ) && ! isset( $_GET['sftec_err'] ) ) { + return; + } + + if ( isset( $_GET['sftec_err'] ) ) { + $msgs = [ + 'no_file' => __( 'No file was uploaded.', 'speakers-for-tec' ), + 'read_fail' => __( 'Could not read the uploaded file.', 'speakers-for-tec' ), + ]; + $msg = $msgs[ $_GET['sftec_err'] ] ?? __( 'An unknown error occurred.', 'speakers-for-tec' ); + printf( + '

%s %s

', + esc_html__( 'Import failed:', 'speakers-for-tec' ), + esc_html( $msg ) + ); + return; + } + + printf( + '

' + . __( 'Import complete — %d created, %d skipped (already exist), %d errors.', 'speakers-for-tec' ) + . '

', + (int) $_GET['sftec_created'], + (int) $_GET['sftec_skipped'], + (int) $_GET['sftec_errors'] + ); + } +} + +new Speakers_For_TEC();