add_action( 'init', $this, 'register_shortcode' ); $loader->add_action( 'init', $this, 'register_block' ); } public function register_shortcode(): void { add_shortcode( 'acc_featured_people', [ $this, 'render' ] ); } public function register_block(): void { if ( ! function_exists( 'register_block_type' ) ) { return; } wp_register_script( 'acc-block-featured-people', false, [ 'wp-blocks', 'wp-element', 'wp-server-side-render', 'wp-block-editor', 'wp-components', 'wp-i18n' ], ACADEMICCREW_VERSION, true ); wp_add_inline_script( 'acc-block-featured-people', $this->editor_script() ); register_block_type( 'academiccrew/featured-people', [ 'api_version' => 2, 'title' => __( 'AcademicCrew: Featured People', 'academiccrew' ), 'description' => __( 'Faculty + rotating postdoc/PhD showcase. Faculty cards carry a red square marker.', 'academiccrew' ), 'category' => 'academiccrew', 'icon' => 'groups', 'supports' => [ 'html' => false, 'align' => [ 'wide', 'full' ] ], 'attributes' => [ 'count' => [ 'type' => 'integer', 'default' => 12 ], 'columns' => [ 'type' => 'integer', 'default' => 6 ], ], 'render_callback' => [ $this, 'render' ], 'editor_script' => 'acc-block-featured-people', 'editor_style_handles' => [ 'acc-tabler-icons', 'acc-system', 'acc-portrait', 'acc-members-archive', 'acc-featured-people' ], ] ); } // ------------------------------------------------------------------------- // Render (block callback + shortcode handler) // ------------------------------------------------------------------------- public function render( array $atts ): string { $atts = shortcode_atts( [ 'count' => 12, 'columns' => 6 ], $atts, 'acc_featured_people' ); $count = max( 3, min( 20, (int) $atts['count'] ) ); $columns = max( 2, min( 6, (int) $atts['columns'] ) ); $members = $this->get_members( $count ); if ( ! $members ) { return sprintf( '

%s

', esc_html__( 'No members found.', 'academiccrew' ) ); } $cards = implode( '', array_map( [ $this, 'render_card' ], $members ) ); return sprintf( '
%s
', esc_attr( sprintf( '--acc-fpp-cols:%d', $columns ) ), $cards ); } // ------------------------------------------------------------------------- // Card HTML // ------------------------------------------------------------------------- private function render_card( array $member ): string { $raw_name = get_the_title( $member['post'] ); $name = esc_html( $raw_name ); $role = $member['role']; $colors = RoleRegistry::colors(); $dot_color = $colors[ $role ]['dot'] ?? '#6b7280'; $permalink = esc_url( $member['permalink'] ); if ( $member['avatar_url'] ) { $portrait_inner = sprintf( '%s', esc_url( $member['avatar_url'] ), esc_attr( $raw_name ) ); $portrait_class = 'acc-portrait'; } else { $portrait_inner = esc_html( $this->initials( $raw_name ) ); $portrait_class = 'acc-portrait is-initials'; } return sprintf( '
%s
%s

%s

', esc_attr( $dot_color ), esc_attr( $portrait_class ), $portrait_inner, esc_html( RoleRegistry::label( $role ) ), $permalink, $name ); } // ------------------------------------------------------------------------- // Data // ------------------------------------------------------------------------- private function get_members( int $limit ): array { $all_roles = array_merge( self::FACULTY_ROLES, self::ROTATING_ROLES ); $faculty_set = array_flip( self::FACULTY_ROLES ); $role_order = array_flip( $all_roles ); $posts = get_posts( [ 'post_type' => 'academiccrew_member', 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_acc_member_role', 'value' => $all_roles, 'compare' => 'IN', ], ], ] ); if ( ! $posts ) { return []; } $faculty = []; $rotating = []; foreach ( $posts as $post ) { $role = (string) ( get_post_meta( $post->ID, '_acc_member_role', true ) ?: '' ); $page_id = (int) get_post_meta( $post->ID, '_academiccrew_linked_page_id', true ); $rank = (int) ( get_post_meta( $post->ID, '_acc_role_rank', true ) ?: 999 ); $first_name = (string) ( get_post_meta( $post->ID, '_acc_first_name', true ) ?: '' ); $last_name = (string) ( get_post_meta( $post->ID, '_acc_last_name', true ) ?: '' ); if ( ! $first_name && ! $last_name ) { $parts = array_values( array_filter( preg_split( '/\s+/', trim( $post->post_title ) ) ) ); $last_name = count( $parts ) > 1 ? array_pop( $parts ) : ( $parts[0] ?? '' ); $first_name = implode( ' ', $parts ); } $member = [ 'post' => $post, 'role' => $role, 'is_faculty' => isset( $faculty_set[ $role ] ), 'avatar_url' => (string) ( get_post_meta( $post->ID, '_acc_avatar_url', true ) ?: '' ), 'affiliation' => (string) ( get_post_meta( $post->ID, '_acc_department', true ) ?: '' ), 'pub_count' => (int) ( get_post_meta( $post->ID, '_acc_pub_count', true ) ?: 0 ), 'permalink' => $page_id ? (string) get_permalink( $page_id ) : (string) get_permalink( $post ), 'order' => $role_order[ $role ] ?? 99, 'rank' => $rank > 0 ? $rank : 999, 'first_name' => $first_name, 'last_name' => $last_name, ]; if ( isset( $faculty_set[ $role ] ) ) { $faculty[] = $member; } else { $rotating[] = $member; } } // Faculty sorted by role → rank (lower first, 999 = default/last) → last name → first name. usort( $faculty, static function ( $a, $b ) { if ( $a['order'] !== $b['order'] ) return $a['order'] - $b['order']; if ( $a['rank'] !== $b['rank'] ) return $a['rank'] - $b['rank']; $cmp = strcmp( $a['last_name'], $b['last_name'] ); if ( $cmp !== 0 ) return $cmp; return strcmp( $a['first_name'], $b['first_name'] ); } ); // Rotating pool: deterministic daily shuffle so the same faces // appear throughout the day but change every 24 hours. if ( $rotating ) { $seed = (int) date( 'z' ) + (int) date( 'Y' ) * 366; $rotating = $this->seeded_shuffle( $rotating, $seed ); } return array_slice( array_merge( $faculty, $rotating ), 0, $limit ); } private function seeded_shuffle( array $arr, int $seed ): array { mt_srand( $seed ); for ( $i = count( $arr ) - 1; $i > 0; $i-- ) { $j = mt_rand( 0, $i ); [ $arr[$i], $arr[$j] ] = [ $arr[$j], $arr[$i] ]; } mt_srand(); return $arr; } private function initials( string $name ): string { $parts = preg_split( '/\s+/', trim( $name ), -1, PREG_SPLIT_NO_EMPTY ); if ( count( $parts ) >= 2 ) { return mb_strtoupper( mb_substr( $parts[0], 0, 1 ) . mb_substr( end( $parts ), 0, 1 ) ); } return mb_strtoupper( mb_substr( $name, 0, 2 ) ); } // ------------------------------------------------------------------------- // Gutenberg editor script // ------------------------------------------------------------------------- private function editor_script(): string { return <<<'JS' (function(blocks, el, SSR, blockEditor, components, i18n) { var __ = i18n.__; var InspectorControls = blockEditor.InspectorControls; var useBlockProps = blockEditor.useBlockProps; var PanelBody = components.PanelBody; var RangeControl = components.RangeControl; blocks.registerBlockType('academiccrew/featured-people', { edit: function(props) { var attrs = props.attributes; var setA = props.setAttributes; var blockProps = useBlockProps({ style: { position: 'relative' } }); return el('div', blockProps, el(InspectorControls, { key: 'controls' }, el(PanelBody, { title: __('Settings', 'academiccrew'), initialOpen: true }, el(RangeControl, { label: __('Number of people', 'academiccrew'), value: attrs.count, min: 3, max: 20, step: 1, onChange: function(v) { setA({ count: v }); } }), el(RangeControl, { label: __('Grid columns', 'academiccrew'), value: attrs.columns, min: 2, max: 6, step: 1, onChange: function(v) { setA({ columns: v }); } }) ) ), el('div', { key: 'preview', style: { pointerEvents: 'none' } }, el(SSR, { block: 'academiccrew/featured-people', attributes: attrs }) ) ); }, save: function() { return null; } }); }(wp.blocks, wp.element.createElement, wp.serverSideRender, wp.blockEditor, wp.components, wp.i18n)); JS; } }