add_action( 'init', $this, 'register_shortcode' ); $loader->add_action( 'init', $this, 'register_block' ); } public function register_shortcode(): void { add_shortcode( 'acc_recent_publications', [ $this, 'render' ] ); } public function register_block(): void { if ( ! function_exists( 'register_block_type' ) ) { return; } wp_register_script( 'acc-block-recent-publications', 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-recent-publications', $this->editor_script() ); register_block_type( 'academiccrew/recent-publications', [ 'api_version' => 2, 'title' => __( 'AcademicCrew: Recent Publications', 'academiccrew' ), 'description' => __( 'List of the most recent publications, grouped by year.', 'academiccrew' ), 'category' => 'academiccrew', 'icon' => 'book-alt', 'supports' => [ 'html' => false, 'align' => [ 'wide', 'full' ] ], 'attributes' => [ 'count' => [ 'type' => 'integer', 'default' => 5 ], ], 'render_callback' => [ $this, 'render' ], 'editor_script' => 'acc-block-recent-publications', 'editor_style_handles' => [ 'acc-tabler-icons', 'acc-system', 'acc-member-profile' ], ] ); } // ------------------------------------------------------------------------- // Render (block callback + shortcode handler) // ------------------------------------------------------------------------- public function render( array $atts ): string { $atts = shortcode_atts( [ 'count' => 5 ], $atts, 'acc_recent_publications' ); $count = max( 1, min( 50, (int) $atts['count'] ) ); $rows = $this->query_recent( $count ); if ( ! $rows ) { return sprintf( '

%s

', esc_html__( 'No publications found.', 'academiccrew' ) ); } $html = '
'; foreach ( $rows as $row ) { $html .= $this->render_pub_item( $row ); } $html .= '
'; return $html; } // ------------------------------------------------------------------------- // Publication item HTML (matches member-profile.js buildPubItem output) // ------------------------------------------------------------------------- private function render_pub_item( array $row ): string { $type = (string) ( $row['pub_type'] ?? '' ); $tc = $this->type_class( $type ); $type_label = $this->type_labels()[ $type ] ?? ( $type ?: __( 'Other', 'academiccrew' ) ); $type_badge = sprintf( '%s', esc_attr( $tc ), esc_html( $type_label ) ); $doi_url = ! empty( $row['doi'] ) ? 'https://doi.org/' . $row['doi'] : ''; $title_url = $doi_url ?: (string) ( $row['url'] ?? '' ); $title_el = $title_url ? sprintf( '%s', esc_url( $title_url ), esc_html( $row['title'] ?? '' ) ) : esc_html( $row['title'] ?? '' ); $raw_authors = json_decode( $row['authors_json'] ?? '[]', true ) ?: []; $authors_html = $raw_authors ? '

' . $this->format_authors_short( $raw_authors ) . '

' : ''; $citations_el = ! empty( $row['citations'] ) ? sprintf( ' %d', (int) $row['citations'] ) : ''; $venue_el = ( ! empty( $row['venue'] ) || $citations_el ) ? sprintf( '

%s%s

', ! empty( $row['venue'] ) ? esc_html( $row['venue'] ) : '', $citations_el ) : ''; $ext = ''; $doi_btn = $doi_url ? sprintf( 'DOI%s', esc_url( $doi_url ), $ext ) : ( ! empty( $row['url'] ) ? sprintf( 'Link%s', esc_url( $row['url'] ), $ext ) : '' ); $pdf_btn = ! empty( $row['pdf_url'] ) ? sprintf( 'PDF%s', esc_url( $row['pdf_url'] ), $ext ) : ''; $tags_html = ''; if ( ! empty( $row['tags'] ) ) { $badges = array_map( fn( $t ) => sprintf( '%s', esc_attr( $t['color'] ), esc_html( $t['name'] ) ), $row['tags'] ); $tags_html = '

' . implode( '', $badges ) . '

'; } $kw_arr = ! empty( $row['keywords'] ) ? ( json_decode( $row['keywords'], true ) ?: [] ) : []; $keywords_el = $kw_arr ? '

' . esc_html( implode( ', ', $kw_arr ) ) . '

' : ''; return sprintf( '
%s

%s

%s%s%s%s
%s%s
', $type_badge, $title_el, $authors_html, $venue_el, $keywords_el, $tags_html, $doi_btn, $pdf_btn ); } // ------------------------------------------------------------------------- // Data // ------------------------------------------------------------------------- private function query_recent( int $limit ): array { global $wpdb; $table = esc_sql( $wpdb->prefix . 'academiccrew_publications' ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE title IS NOT NULL AND title != '' ORDER BY year DESC, id DESC LIMIT %d", $limit ), ARRAY_A ) ?: []; if ( $rows ) { $pub_ids = array_column( $rows, 'id' ); $placeholders = implode( ',', array_fill( 0, count( $pub_ids ), '%d' ) ); $tag_rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore "SELECT r.publication_id, t.name, t.color FROM {$wpdb->prefix}academiccrew_pub_tag_relations r INNER JOIN {$wpdb->prefix}academiccrew_pub_tags t ON t.id = r.tag_id WHERE r.publication_id IN ({$placeholders}) ORDER BY t.name ASC", // phpcs:ignore ...$pub_ids ), ARRAY_A ) ?: []; $tag_map = []; foreach ( $tag_rows as $tr ) { $tag_map[ (int) $tr['publication_id'] ][] = $tr; } foreach ( $rows as &$row ) { $row['tags'] = $tag_map[ (int) $row['id'] ] ?? []; } unset( $row ); } return $rows; } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private function type_class( string $type ): string { if ( str_contains( $type, 'journal' ) ) return 't-journal'; if ( str_contains( $type, 'conference' ) ) return 't-conf'; if ( $type === 'book-chapter' ) return 't-chapter'; if ( $type === 'book' ) return 't-book'; if ( $type === 'preprint' ) return 't-preprint'; return 't-other'; } private function type_labels(): array { return [ 'journal-article' => __( 'Journal', 'academiccrew' ), 'conference-paper' => __( 'Conference', 'academiccrew' ), 'book-chapter' => __( 'Book Chapter','academiccrew' ), 'book' => __( 'Book', 'academiccrew' ), 'preprint' => __( 'Preprint', 'academiccrew' ), 'thesis' => __( 'Thesis', 'academiccrew' ), 'report' => __( 'Report', 'academiccrew' ), ]; } private function format_authors_short( array $authors, int $max = 3 ): string { $shown = array_slice( $authors, 0, $max ); $parts = array_map( static function ( $a ): string { if ( ! is_array( $a ) ) { return esc_html( (string) $a ); } $first = (string) ( $a['firstName'] ?? '' ); $last = (string) ( $a['lastName'] ?? ( $a['name'] ?? '' ) ); $initial = $first ? esc_html( mb_substr( $first, 0, 1 ) ) . '. ' : ''; return $initial . esc_html( $last ); }, $shown ); $tail = count( $authors ) > $max ? sprintf( ', %s (%d)', esc_html__( 'et al.', 'academiccrew' ), count( $authors ) ) : ''; return implode( ', ', $parts ) . $tail; } // ------------------------------------------------------------------------- // 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/recent-publications', { 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 publications', 'academiccrew'), value: attrs.count, min: 1, max: 50, step: 1, onChange: function(v) { setA({ count: v }); } }) ) ), el('div', { key: 'preview', style: { pointerEvents: 'none' } }, el(SSR, { block: 'academiccrew/recent-publications', attributes: attrs }) ) ); }, save: function() { return null; } }); }(wp.blocks, wp.element.createElement, wp.serverSideRender, wp.blockEditor, wp.components, wp.i18n)); JS; } }