plugin_name = $plugin_name;
$this->version = $version;
}
public function enqueue_styles() {
wp_enqueue_style($this->plugin_name, plugin_dir_url(__FILE__).'css/aps-public.css', array(), $this->version . '-' . filemtime(plugin_dir_path(__FILE__) . 'css/aps-public.css'), 'all');
$options = get_option( $this->plugin_name );
// Enqueue Google Fonts if needed
if ( ! empty( $options['font_family'] ) ) {
$font = $options['font_family'];
$allowed_gfonts = array( 'Inter', 'Roboto', 'Open Sans', 'Merriweather' );
if ( in_array( $font, $allowed_gfonts, true ) ) {
wp_enqueue_style(
$this->plugin_name . '-google-font',
'https://fonts.googleapis.com/css2?family=' . rawurlencode( $font ) . ':wght@400;600;700&display=swap',
array(),
$this->version
);
}
}
// Inject dynamic design CSS here, before wp_head fires, so it is actually output.
// wp_add_inline_style() must be called during wp_enqueue_scripts, not inside a shortcode callback.
$primary_color = isset( $options['primary_color'] ) ? $options['primary_color'] : '#2271b1';
$card_bg = isset( $options['card_bg_color'] ) ? $options['card_bg_color'] : '#ffffff';
$card_border = isset( $options['card_border_color'] ) ? $options['card_border_color'] : '#dcdcde';
$text_color = isset( $options['text_color'] ) ? $options['text_color'] : '#1d2327';
$meta_color = isset( $options['meta_text_color'] ) ? $options['meta_text_color'] : '#646970';
$font_family = isset( $options['font_family'] ) ? $options['font_family'] : 'system';
$font_size = isset( $options['font_size'] ) ? $options['font_size'] : 'medium';
$border_radius = isset( $options['border_radius'] ) ? $options['border_radius'] : 'medium';
$radius_map = array( 'none' => '0', 'small' => '4px', 'medium' => '8px', 'large' => '12px' );
$radius_value = isset( $radius_map[ $border_radius ] ) ? $radius_map[ $border_radius ] : '8px';
$size_map = array( 'small' => '0.875rem', 'medium' => '1rem', 'large' => '1.125rem' );
$base_size = isset( $size_map[ $font_size ] ) ? $size_map[ $font_size ] : '1rem';
$allowed_fonts = array( 'system', 'Inter', 'Roboto', 'Open Sans', 'Merriweather' );
if ( ! in_array( $font_family, $allowed_fonts, true ) ) {
$font_family = 'system';
}
$safe_font = esc_attr( $font_family );
$font_stack = $font_family === 'system'
? '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, sans-serif'
: "'" . $safe_font . "', sans-serif";
$acadpush_inline_css = '.dyzc-publications-grid {
--dyzc-primary: ' . esc_attr( $primary_color ) . ';
--dyzc-card-bg: ' . esc_attr( $card_bg ) . ';
--dyzc-card-border: ' . esc_attr( $card_border ) . ';
--dyzc-text: ' . esc_attr( $text_color ) . ';
--dyzc-meta: ' . esc_attr( $meta_color ) . ';
--dyzc-radius: ' . esc_attr( $radius_value ) . ';
--dyzc-base-size: ' . esc_attr( $base_size ) . ';
font-family: ' . esc_attr( $font_stack ) . ';
font-size: ' . esc_attr( $base_size ) . ';
/* Elementor fallback protection */
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.dyzc-publications-grid .dyzc-pub-card {
margin-bottom: 40px !important;
}
.dyzc-publications-grid .dyzc-pub-actions {
display: flex !important;
gap: 12px !important;
}
.dyzc-publications-grid .dyzc-btn {
display: inline-flex !important;
align-items: center;
padding: 10px 18px !important;
text-decoration: none !important;
}';
wp_add_inline_style( $this->plugin_name, $acadpush_inline_css );
}
public function enqueue_scripts() {
wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__).'js/aps-public.js', array('jquery'), $this->version . '-' . filemtime(plugin_dir_path(__FILE__) . 'js/aps-public.js'), true);
}
public function zp_query_vars( $query_vars ) {
$query_vars[] = 'zenodo_page';
return $query_vars;
}
public function display_zenodo_data( $attr ) {
$attr = shortcode_atts( array(
'keyword' => '',
'lang' => '',
'type' => '', // General type: publication, dataset, software, etc.
'subtype' => '' // Specific subtype: article, book, conferencepaper, etc.
), $attr, 'showcase-publications' );
$lang = (strpos(strtolower(get_locale()), 'cs') === 0) ? 'cs' : 'en';
$allowed_langs = array( 'cs', 'en' );
if ( ! empty( $attr['lang'] ) && in_array( strtolower( $attr['lang'] ), $allowed_langs, true ) ) {
$lang = strtolower( $attr['lang'] );
}
$shortcode_specific_keyword = ! empty( $attr['keyword'] ) ? sanitize_text_field( $attr['keyword'] ) : '';
$shortcode_specific_type = ! empty( $attr['type'] ) ? sanitize_text_field( $attr['type'] ) : '';
$shortcode_specific_subtype = ! empty( $attr['subtype'] ) ? sanitize_text_field( $attr['subtype'] ) : '';
// Validate type against Zenodo's controlled vocabulary
$allowed_types = array(
'publication', 'poster', 'presentation', 'dataset',
'image', 'video', 'software', 'lesson',
'physicalobject', 'other'
);
if ( ! empty( $shortcode_specific_type ) && ! in_array( $shortcode_specific_type, $allowed_types, true ) ) {
$shortcode_specific_type = ''; // Invalid type, ignore
}
// Validate subtype (only publication subtypes for now)
$allowed_publication_subtypes = array(
'annotationcollection', 'book', 'section', 'conferencepaper',
'datamanagementplan', 'article', 'patent', 'preprint',
'deliverable', 'milestone', 'proposal', 'report',
'softwaredocumentation', 'taxonomictreatment', 'technicalnote',
'thesis', 'workingpaper', 'other'
);
// Only validate subtype if it's provided
if ( ! empty( $shortcode_specific_subtype ) ) {
if ( ! in_array( $shortcode_specific_subtype, $allowed_publication_subtypes, true ) ) {
$shortcode_specific_subtype = ''; // Invalid subtype, ignore
}
}
$options = get_option($this->plugin_name);
$page = max(1, (int)get_query_var('zenodo_page', 1));
$api_response = $this->zp_retrieve_json($page, $options, $shortcode_specific_keyword, $shortcode_specific_type, $shortcode_specific_subtype);
if ( is_wp_error( $api_response ) ) {
return '
' . esc_html__( 'Unable to load publications. Please try again later.', 'academic-publications-showcase' ) . '
';
}
if ( ! $api_response || ! isset( $api_response->hits->hits ) || empty( $api_response->hits->hits ) ) {
return '' . esc_html( $this->get_label( 'no_results', $lang ) ) . '
';
}
$html = '';
$total_docs = is_object($api_response->hits->total) ? $api_response->hits->total->value : $api_response->hits->total;
$per_page = isset($options['number_publications']) ? max(1, (int)$options['number_publications']) : 10;
$nb_pages = ceil($total_docs / $per_page);
$html .= '';
foreach ($api_response->hits->hits as $hit) {
if (empty($hit->metadata)) continue;
$m = $hit->metadata;
$html .= '
';
// Title
$html .= ''.esc_html($m->title).'
';
// Meta info (date, journal, type)
$meta_parts = array();
if (!empty($m->publication_date)) {
$meta_parts[] = $this->format_date($m->publication_date, $lang);
}
if (!empty($m->resource_type->title)) {
$meta_parts[] = esc_html($m->resource_type->title);
}
$journal = $m->journal->title ?? $m->journal_title ?? '';
if ($journal) {
$meta_parts[] = '' . esc_html($journal) . '';
}
if (!empty($meta_parts)) {
$html .= '' . wp_kses_post( implode(' • ', $meta_parts) ) . '
';
}
// Authors
if (!empty($m->creators)) {
$html .= '';
$author_names = array();
$max_authors = 5;
$creator_count = count($m->creators);
foreach (array_slice($m->creators, 0, $max_authors) as $creator) {
$name = isset($creator->name) ? esc_html($creator->name) : '';
if ($name) {
if (!empty($creator->affiliation)) {
$author_names[] = '' . $name . '';
} else {
$author_names[] = '' . $name . '';
}
}
}
$html .= wp_kses_post( implode(', ', $author_names) );
if ($creator_count > $max_authors) {
$html .= ' et al.';
}
$html .= '
';
}
// Abstract (collapsible)
if (!empty($m->description)) {
$clean_desc = wp_strip_all_tags($m->description);
$short_desc = mb_strlen($clean_desc) > 200 ? mb_substr($clean_desc, 0, 200) . '…' : $clean_desc;
$html .= '';
$html .= '
' . esc_html($short_desc) . '
';
if (mb_strlen($clean_desc) > 200) {
$html .= '
' . esc_html($clean_desc) . '
';
$html .= '
';
}
$html .= '
';
}
// Badges (tags/keywords)
if (!empty($m->keywords)) {
$html .= '';
foreach (array_slice($m->keywords, 0, 5) as $keyword) {
$html .= '' . esc_html($keyword) . '';
}
$html .= '
';
}
// Action buttons
$html .= ''; // .dyzc-pub-actions
$html .= ''; // .dyzc-pub-card
}
$html .= $this->render_pagination($page, $nb_pages, $lang);
$html .= '
'; // .dyzc-publications-grid
return $html;
}
private function zp_retrieve_json($page, $options, $keyword_override, $type_filter = '', $subtype_filter = '') {
$choice = isset($options['choice']) ? $options['choice'] : 'community';
$id = isset($options['id_community_orcid']) ? trim($options['id_community_orcid']) : '';
$access_token = isset($options['access_token']) ? trim($options['access_token']) : '';
$extra_keyword = isset($options['extra_keyword']) ? trim($options['extra_keyword']) : '';
$size = isset($options['number_publications']) ? max(1, min(100, (int)$options['number_publications'])) : 10;
if (empty($id)) {
return new WP_Error('no_id', 'No Community ID or ORCID configured.');
}
$final_keyword = !empty($keyword_override) ? $keyword_override : $extra_keyword;
$transient_key = 'acadpush_' . md5($choice . $id . $final_keyword . $type_filter . $subtype_filter . $page . $size);
$cached = get_transient($transient_key);
if ($cached !== false) {
return $cached;
}
if ($choice === 'orcid') {
$query = 'creators.orcid:"' . $id . '"';
if (!empty($final_keyword)) {
$query .= ' AND ' . $final_keyword;
}
$url = 'https://zenodo.org/api/records/?q=' . rawurlencode($query) . '&size=' . $size . '&page=' . $page . '&sort=newest';
// Add type and subtype as dedicated URL parameters
if (!empty($type_filter)) {
$url .= '&type=' . rawurlencode($type_filter);
}
if (!empty($subtype_filter)) {
$url .= '&subtype=' . rawurlencode($subtype_filter);
}
} else {
$url = 'https://zenodo.org/api/records/?communities=' . rawurlencode($id) . '&size=' . $size . '&page=' . $page . '&sort=newest';
// Add keyword as q parameter if present
if (!empty($final_keyword)) {
$url .= '&q=' . rawurlencode($final_keyword);
}
// Add type and subtype as dedicated URL parameters
if (!empty($type_filter)) {
$url .= '&type=' . rawurlencode($type_filter);
}
if (!empty($subtype_filter)) {
$url .= '&subtype=' . rawurlencode($subtype_filter);
}
}
$args = array(
'timeout' => 30,
'user-agent' => 'Academic Publications Showcase/' . $this->version . ' (WordPress Plugin)',
'headers' => array('Accept' => 'application/json'),
);
if (!empty($access_token)) {
$args['headers']['Authorization'] = 'Bearer ' . $access_token;
}
$response = wp_remote_get($url, $args);
if (is_wp_error($response)) {
return $response;
}
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
return new WP_Error('http_error', 'HTTP ' . $status_code);
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body);
if (json_last_error() !== JSON_ERROR_NONE) {
return new WP_Error('json_error', 'Invalid JSON response');
}
set_transient($transient_key, $data, HOUR_IN_SECONDS);
return $data;
}
private function render_pagination($page, $nb_pages, $lang) {
if ($nb_pages <= 1) return '';
$pagination_args = array(
'base' => add_query_arg('zenodo_page', '%#%'),
'current' => $page,
'total' => $nb_pages,
'type' => 'list',
'prev_text' => $this->get_label('previous', $lang),
'next_text' => $this->get_label('next', $lang),
);
return '';
}
private function format_date($date_string, $lang) {
$timestamp = strtotime($date_string);
if (!$timestamp) return esc_html($date_string);
$months_cs = array('', 'Leden', 'Únor', 'Březen', 'Duben', 'Květen', 'Červen', 'Červenec', 'Srpen', 'Září', 'Říjen', 'Listopad', 'Prosinec');
$months_en = array('', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');
$month_num = (int)gmdate('n', $timestamp);
$year = gmdate('Y', $timestamp);
$month = ($lang === 'cs') ? $months_cs[$month_num] : $months_en[$month_num];
return $month . ', ' . $year;
}
private function get_label($key, $lang) {
$labels = array(
'cs' => array(
'no_results' => 'Žádné publikace nenalezeny.',
'view_publication' => 'Publikace',
'view_repository' => 'Repozitář',
'show_more' => 'Zobrazit více',
'show_less' => 'Zobrazit méně',
'previous' => 'Předchozí',
'next' => 'Další',
),
'en' => array(
'no_results' => 'No publications found.',
'view_publication' => 'Publication',
'view_repository' => 'Repository',
'show_more' => 'Show more',
'show_less' => 'Show less',
'previous' => 'Previous',
'next' => 'Next',
),
);
$lang_labels = isset($labels[$lang]) ? $labels[$lang] : $labels['en'];
return isset($lang_labels[$key]) ? $lang_labels[$key] : '';
}
}