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(
'
',
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(
'
',
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(
'
',
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;
}
}