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 '
';
+
+ $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
+
+
+ - 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(
+ '',
+ 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();