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