float, 'label' => '+107%', 'improved' => bool] or null. */ function sahcs_metric_change(array $item) { $before = sahcs_parse_number(isset($item['before']) ? $item['before'] : ''); $after = sahcs_parse_number(isset($item['after']) ? $item['after'] : ''); if ($before === null || $after === null || (float) $before === 0.0) { return null; } $pct = ($after - $before) / abs($before) * 100; if (abs($pct) < 0.05) { return null; } $better_lower = isset($item['better']) && $item['better'] === 'lower'; $improved = $better_lower ? $pct < 0 : $pct > 0; $rounded = abs($pct) >= 10 ? round(abs($pct)) : round(abs($pct), 1); $label = ($pct > 0 ? '+' : "\u{2212}") . number_format_i18n($rounded, (abs($pct) >= 10 ? 0 : 1)) . '%'; return ['pct' => $pct, 'label' => $label, 'improved' => $improved]; } function sahcs_format_value($value, array $item = []) { $value = sahcs_trim((string) $value); if ($value === '') { return ''; } $prefix = isset($item['prefix']) ? (string) $item['prefix'] : ''; $suffix = isset($item['suffix']) ? (string) $item['suffix'] : ''; return $prefix . $value . $suffix; } /* ------------------------------------------------------------------ * Small helpers * ---------------------------------------------------------------- */ /** Safe list of allowed heading tags for cards and section titles. */ function sahcs_heading_tag($tag, $default = 'h3') { return in_array($tag, ['h2', 'h3', 'h4', 'p'], true) ? $tag : $default; } /** * A short summary of a case study: the manual excerpt if there is one, * otherwise the "At a glance" text or the first paragraph. */ function sahcs_card_excerpt($post, $words = 24) { $post = get_post($post); if (!$post) { return ''; } if (has_excerpt($post)) { return wp_trim_words(get_the_excerpt($post), $words); } $text = ''; $walk = function ($blocks) use (&$walk, &$text) { foreach ($blocks as $b) { if ($text !== '') { return; } if ($b['blockName'] === 'sahcs/hero' && !empty($b['attrs']['intro'])) { $text = $b['attrs']['intro']; return; } if ($b['blockName'] === 'core/paragraph') { $t = sahcs_trim(wp_strip_all_tags($b['innerHTML'])); if ($t !== '') { $text = $t; return; } } if (!empty($b['innerBlocks'])) { $walk($b['innerBlocks']); } } }; $walk(parse_blocks($post->post_content)); return wp_trim_words($text, $words); } /** Terms of a case study, cached by WordPress. */ function sahcs_post_terms($post_id) { $terms = get_the_terms($post_id, SAHCS_TAX); return (is_array($terms)) ? $terms : []; } /* ------------------------------------------------------------------ * Case study card * ---------------------------------------------------------------- */ /** * @param int $post_id * @param array $args heading (h2|h3|h4), excerpt (bool), eager (bool), stat (bool) */ function sahcs_card($post_id, array $args = []) { $args = array_merge(['heading' => 'h3', 'excerpt' => true, 'eager' => false, 'priority' => false, 'stat' => true], $args); $post = get_post($post_id); if (!$post) { return ''; } $tag = sahcs_heading_tag($args['heading'], 'h3'); $url = get_permalink($post); $title = get_the_title($post); $terms = sahcs_post_terms($post->ID); $media = ''; if (has_post_thumbnail($post)) { $img_attrs = [ 'class' => 'sahcs-card__img', 'loading' => $args['eager'] ? 'eager' : 'lazy', 'sizes' => '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw', 'alt' => '', // Some themes add inline sizes to every image; these win over them. 'style' => 'width:100%;height:100%;max-width:100%', ]; if ($args['priority']) { $img_attrs['fetchpriority'] = 'high'; } $media = get_the_post_thumbnail($post, 'medium_large', $img_attrs); } if ($media === '') { $initial = function_exists('mb_substr') ? mb_substr(wp_strip_all_tags($title), 0, 1) : substr(wp_strip_all_tags($title), 0, 1); $media = ''; } $term_slugs = wp_list_pluck($terms, 'slug'); $out = '
'; $out .= '
' . $media . '
'; $out .= '
'; if ($terms) { $out .= '

'; foreach (array_slice($terms, 0, 2) as $t) { $out .= '' . esc_html($t->name) . ''; } $out .= '

'; } $out .= '<' . $tag . ' class="sahcs-card__title">' . esc_html(wp_strip_all_tags($title)) . ''; if ($args['stat']) { $hl = get_post_meta($post->ID, '_sahcs_highlight', true); if (is_array($hl) && !empty($hl['value'])) { $out .= '

' . esc_html($hl['value']) . ' ' . esc_html(isset($hl['label']) ? $hl['label'] : '') . '

'; } } if ($args['excerpt']) { $excerpt = sahcs_card_excerpt($post); if ($excerpt !== '') { $out .= '

' . esc_html($excerpt) . '

'; } } $out .= ''; $out .= '
'; return $out; } /* ------------------------------------------------------------------ * Grid (used by the shortcode, the "Case Studies" block and Related) * ---------------------------------------------------------------- */ function sahcs_grid_defaults() { return [ 'limit' => 6, 'category' => '', 'ids' => '', 'exclude' => '', // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- shortcode attribute default, not a query argument 'columns' => 3, 'layout' => 'grid', // grid | list 'filters' => false, 'orderby' => 'latest', // latest | oldest | title | random | menu 'excerpt' => true, 'button' => false, 'button_text' => '', 'heading' => '', // '' = automatic: h2 without a title, h3 under a title 'title' => '', ]; } function sahcs_bool($v) { if (is_bool($v)) { return $v; } return in_array(strtolower((string) $v), ['1', 'yes', 'true', 'on'], true); } function sahcs_grid(array $args) { $a = array_merge(sahcs_grid_defaults(), $args); $a['limit'] = max(1, min(60, (int) $a['limit'])); $a['columns'] = max(1, min(4, (int) $a['columns'])); $a['layout'] = $a['layout'] === 'list' ? 'list' : 'grid'; $a['filters'] = sahcs_bool($a['filters']); $a['excerpt'] = sahcs_bool($a['excerpt']); $a['button'] = sahcs_bool($a['button']); $query = [ 'post_type' => SAHCS_CPT, 'post_status' => 'publish', 'posts_per_page' => $a['limit'], 'ignore_sticky_posts' => true, 'no_found_rows' => true, ]; $ids = array_values(array_unique(array_filter(array_map('sahcs_localize_id', array_map('absint', explode(',', (string) $a['ids'])))))); if ($ids) { $query['post__in'] = $ids; $query['orderby'] = 'post__in'; $query['posts_per_page'] = count($ids); } else { switch ($a['orderby']) { case 'oldest': $query['orderby'] = 'date'; $query['order'] = 'ASC'; break; case 'title': $query['orderby'] = 'title'; $query['order'] = 'ASC'; break; case 'random': $query['orderby'] = 'rand'; break; case 'menu': $query['orderby'] = ['menu_order' => 'ASC', 'date' => 'DESC']; break; default: $query['orderby'] = 'date'; $query['order'] = 'DESC'; } } $cats = array_filter(array_map('sanitize_title', explode(',', (string) $a['category']))); if ($cats) { $query['tax_query'] = [[ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'taxonomy' => SAHCS_TAX, 'field' => 'slug', 'terms' => $cats, ]]; } $exclude = array_filter(array_map('absint', explode(',', (string) $a['exclude']))); if ($exclude && (int) $query['posts_per_page'] > 0) { // Ask for a few extra and drop the excluded ones here (faster than NOT IN). $query['posts_per_page'] = (int) $query['posts_per_page'] + count($exclude); } $q = new WP_Query($query); if ($exclude) { $keep = (int) $a['limit']; $q->posts = array_values(array_filter($q->posts, function ($p) use ($exclude) { return !in_array((int) $p->ID, $exclude, true); })); if ($keep > 0 && empty($ids)) { $q->posts = array_slice($q->posts, 0, $keep); } $q->post_count = count($q->posts); } sahcs_enqueue_front(); if (!$q->have_posts()) { return ''; } // Heading levels follow the page outline: the page title is the H1, a grid // title is an H2 and its cards H3; without a grid title the cards are H2. $card_heading = (isset($a['heading']) && $a['heading'] !== '') ? sahcs_heading_tag($a['heading'], 'h3') : ($a['title'] !== '' ? 'h3' : 'h2'); $out = '
'; if ($a['title'] !== '') { $out .= '

' . esc_html($a['title']) . '

'; } if ($a['filters'] && !$ids) { $terms = []; foreach ($q->posts as $p) { foreach (sahcs_post_terms($p->ID) as $t) { $terms[$t->slug] = $t->name; } } if (count($terms) > 1) { asort($terms); $out .= '
'; } } $cols_class = $a['layout'] === 'list' ? '' : ' sahcs-cols-' . (int) $a['columns']; $out .= '
'; foreach ($q->posts as $p) { $out .= sahcs_card($p->ID, ['heading' => $card_heading, 'excerpt' => $a['excerpt']]); } $out .= '
'; $out .= ''; if ($a['button']) { $text = $a['button_text'] !== '' ? $a['button_text'] : __('View all case studies', 'siteaudithub-case-studies'); $out .= '

' . esc_html($text) . '

'; } $out .= '
'; wp_reset_postdata(); return $out; } /* ------------------------------------------------------------------ * Listing page (archive and category pages) * ---------------------------------------------------------------- */ function sahcs_render_archive() { global $wp_query; $s = sahcs_get_settings(); sahcs_enqueue_front(); $term = is_tax(SAHCS_TAX) ? get_queried_object() : null; if ($term) { /* translators: %s: category name, e.g. Shopify */ $title = sprintf(__('%s case studies', 'siteaudithub-case-studies'), $term->name); $intro = $term->description !== '' ? $term->description : $s['archive_intro']; } else { $title = $s['archive_title'] !== '' ? $s['archive_title'] : __('Case Studies', 'siteaudithub-case-studies'); $intro = $s['archive_intro']; } $out = '
' . sahcs_page_script(); $out .= '
'; if ($term) { $out .= ''; } $out .= '

' . esc_html($title) . '

'; if ($intro !== '') { $out .= '
' . wpautop(esc_html($intro)) . '
'; } $out .= '
'; if (!empty($s['show_filters'])) { $out .= sahcs_archive_filters($term); } $out .= '
'; if (have_posts()) { $layout = (isset($s['archive_layout']) && $s['archive_layout'] === 'list') ? 'list' : 'grid'; $out .= '
'; $i = 0; while (have_posts()) { the_post(); $out .= sahcs_card(get_the_ID(), ['heading' => 'h2', 'eager' => $i < (int) $s['columns'], 'priority' => $i === 0]); $i++; } $out .= '
'; $links = paginate_links([ 'type' => 'list', 'prev_text' => ' ' . esc_html__('Previous', 'siteaudithub-case-studies'), 'next_text' => esc_html__('Next', 'siteaudithub-case-studies') . ' ', 'total' => (int) $wp_query->max_num_pages, ]); if ($links) { $out .= ''; } wp_reset_postdata(); } else { $out .= '

' . esc_html__('No case studies found yet.', 'siteaudithub-case-studies') . '

'; } $out .= '
'; return $out; } function sahcs_archive_filters($current_term = null) { $terms = get_terms(['taxonomy' => SAHCS_TAX, 'hide_empty' => true]); if (is_wp_error($terms) || count($terms) < 1) { return ''; } $all_url = get_post_type_archive_link(SAHCS_CPT); $out = ''; return $out; } /* ------------------------------------------------------------------ * Related case studies * ---------------------------------------------------------------- */ /** * Related case studies. * $args['ids'] - chosen case studies in this order (empty = automatic: same category first). * $args['layout'] - 'grid' or 'list'; empty = the Settings choice. */ function sahcs_related($post_id, $count, $title, $args = []) { $count = max(1, min(12, (int) $count)); $chosen = isset($args['ids']) ? array_values(array_unique(array_filter(array_map('intval', (array) $args['ids'])))) : []; $layout = isset($args['layout']) && in_array($args['layout'], ['grid', 'list'], true) ? $args['layout'] : sahcs_get_settings('related_layout'); $layout = $layout === 'list' ? 'list' : 'grid'; $ids = []; if ($chosen) { // Only published case studies, never the one being viewed, in the chosen order. foreach ($chosen as $id) { $id = sahcs_localize_id($id); // same case study in the page's language if ($id !== (int) $post_id && get_post_type($id) === SAHCS_CPT && get_post_status($id) === 'publish') { $ids[] = $id; } } } if (!$ids) { $term_ids = wp_list_pluck(sahcs_post_terms($post_id), 'term_id'); $query = [ 'post_type' => SAHCS_CPT, 'post_status' => 'publish', 'posts_per_page' => $count + 1, 'fields' => 'ids', 'ignore_sticky_posts' => true, 'no_found_rows' => true, ]; $ids = []; if ($term_ids) { $query['tax_query'] = [[ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'taxonomy' => SAHCS_TAX, 'field' => 'term_id', 'terms' => $term_ids, ]]; $ids = array_diff(array_map('intval', (new WP_Query($query))->posts), [(int) $post_id]); $ids = array_slice(array_values($ids), 0, $count); unset($query['tax_query']); } // Top up with the latest case studies if the category has too few. if (count($ids) < $count) { $skip = array_merge([(int) $post_id], $ids); $query['posts_per_page'] = $count + count($skip); $more = array_diff(array_map('intval', (new WP_Query($query))->posts), $skip); $ids = array_merge($ids, array_slice(array_values($more), 0, $count - count($ids))); } } if (!$ids) { return ''; } // Columns follow the number asked for ("How many"), not the number found, // so one related case study keeps its normal card size and the other // places stay empty instead of one card stretching across the row. $cols = min(3, max(count($ids), $count)); $grid_class = $layout === 'list' ? 'sahcs-grid sahcs-grid--list' : 'sahcs-grid sahcs-grid--grid sahcs-cols-' . (int) $cols; $out = ''; return $out; }