| 1 |
<?php |
| 2 |
/** |
| 3 |
* Top Posts Renderer |
| 4 |
* |
| 5 |
* Shared HTML helper used by Top Winning Posts and Top Losing Posts so |
| 6 |
* both render identically. Not a section itself — purely a stateless |
| 7 |
* presentation helper. |
| 8 |
* |
| 9 |
* @package ThinkRank |
| 10 |
* @subpackage SEO\Email_Report_Sections |
| 11 |
* @since 1.9.0 |
| 12 |
*/ |
| 13 |
|
| 14 |
declare(strict_types=1); |
| 15 |
|
| 16 |
namespace ThinkRank\SEO\Email_Report_Sections; |
| 17 |
|
| 18 |
if (!defined('ABSPATH')) { |
| 19 |
exit; |
| 20 |
} |
| 21 |
|
| 22 |
final class Top_Posts_Renderer { |
| 23 |
|
| 24 |
/** |
| 25 |
* Render a page table (page, clicks, click change vs previous period). |
| 26 |
* |
| 27 |
* @param array $rows Each row: ['url'|'page' => string, 'clicks' => int, 'change' => ?int, 'title' => ?string] |
| 28 |
* @param string $variant 'gain' | 'loss' — controls accent color only. |
| 29 |
*/ |
| 30 |
public static function render(array $rows, string $variant = 'gain'): string { |
| 31 |
if (empty($rows)) { |
| 32 |
return ''; |
| 33 |
} |
| 34 |
|
| 35 |
$accent = $variant === 'loss' ? '#dc2626' : '#16a34a'; |
| 36 |
|
| 37 |
$html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">'; |
| 38 |
$html .= '<tr>' |
| 39 |
. self::th(__('Page', 'thinkrank'), 'left') |
| 40 |
. self::th(__('Clicks', 'thinkrank'), 'right') |
| 41 |
. self::th(__('Change', 'thinkrank'), 'right') |
| 42 |
. '</tr>'; |
| 43 |
|
| 44 |
foreach ($rows as $row) { |
| 45 |
$url = (string) ($row['url'] ?? $row['page'] ?? ''); |
| 46 |
$title = (string) ($row['title'] ?? ''); |
| 47 |
$clicks = (int) ($row['clicks'] ?? 0); |
| 48 |
$display = $title !== '' ? $title : $url; |
| 49 |
|
| 50 |
$html .= '<tr>' |
| 51 |
. '<td style="padding:10px;border-bottom:1px solid #f3f4f6;">' |
| 52 |
. ($url !== '' |
| 53 |
? '<a href="' . esc_url($url) . '" style="color:#111827;text-decoration:none;">' . esc_html($display) . '</a>' |
| 54 |
: '<span style="color:#111827;">' . esc_html($display) . '</span>') |
| 55 |
. '</td>' |
| 56 |
. '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;color:#374151;font:600 13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;">' |
| 57 |
. esc_html(number_format_i18n($clicks)) |
| 58 |
. '</td>' |
| 59 |
. self::change_cell($row['change'] ?? null, $accent) |
| 60 |
. '</tr>'; |
| 61 |
} |
| 62 |
|
| 63 |
$html .= '</table>'; |
| 64 |
return $html; |
| 65 |
} |
| 66 |
|
| 67 |
/** |
| 68 |
* Render a keyword table (query, position, clicks, click change). |
| 69 |
* |
| 70 |
* @param array $rows Each row: ['query' => string, 'position' => ?float, 'clicks' => int, 'change' => ?int] |
| 71 |
* @param string $variant 'gain' | 'loss' |
| 72 |
*/ |
| 73 |
public static function render_keywords(array $rows, string $variant = 'gain'): string { |
| 74 |
if (empty($rows)) { |
| 75 |
return ''; |
| 76 |
} |
| 77 |
|
| 78 |
$accent = $variant === 'loss' ? '#dc2626' : '#16a34a'; |
| 79 |
|
| 80 |
$html = '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">'; |
| 81 |
$html .= '<tr>' |
| 82 |
. self::th(__('Keyword', 'thinkrank'), 'left') |
| 83 |
. self::th(__('Position', 'thinkrank'), 'right') |
| 84 |
. self::th(__('Clicks', 'thinkrank'), 'right') |
| 85 |
. self::th(__('Change', 'thinkrank'), 'right') |
| 86 |
. '</tr>'; |
| 87 |
|
| 88 |
foreach ($rows as $row) { |
| 89 |
$query = (string) ($row['query'] ?? ($row['keys'][0] ?? '')); |
| 90 |
// Null position means "no data this period" (e.g. a keyword that |
| 91 |
// dropped out entirely) — a literal 0.0 would read as rank #1. |
| 92 |
$position = isset($row['position']) |
| 93 |
? number_format_i18n((float) $row['position'], 1) |
| 94 |
: '—'; |
| 95 |
$clicks = (int) ($row['clicks'] ?? 0); |
| 96 |
if ($query === '') { |
| 97 |
continue; |
| 98 |
} |
| 99 |
$html .= '<tr>' |
| 100 |
. '<td style="padding:10px;border-bottom:1px solid #f3f4f6;color:#111827;">' . esc_html($query) . '</td>' |
| 101 |
. '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;color:#374151;font:600 13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;">' . esc_html($position) . '</td>' |
| 102 |
. '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;color:#374151;">' . esc_html(number_format_i18n($clicks)) . '</td>' |
| 103 |
. self::change_cell($row['change'] ?? null, $accent) |
| 104 |
. '</tr>'; |
| 105 |
} |
| 106 |
|
| 107 |
$html .= '</table>'; |
| 108 |
return $html; |
| 109 |
} |
| 110 |
|
| 111 |
/** |
| 112 |
* Table header cell. |
| 113 |
*/ |
| 114 |
private static function th(string $label, string $align): string { |
| 115 |
return '<th style="text-align:' . esc_attr($align) . ';padding:8px 10px;border-bottom:1px solid #e5e7eb;font:600 12px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#6b7280;text-transform:uppercase;letter-spacing:.04em;">' |
| 116 |
. esc_html($label) . '</th>'; |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Signed click-change cell. Positive renders green with ▲, negative red |
| 121 |
* with ▼. A null/zero change renders a neutral dash. |
| 122 |
*/ |
| 123 |
private static function change_cell($change, string $accent): string { |
| 124 |
$td = '<td style="padding:10px;border-bottom:1px solid #f3f4f6;text-align:right;font:600 13px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:%s;">%s</td>'; |
| 125 |
|
| 126 |
if ($change === null || (int) $change === 0) { |
| 127 |
return sprintf($td, '#9ca3af', '—'); |
| 128 |
} |
| 129 |
$change = (int) $change; |
| 130 |
$arrow = $change > 0 ? '▲' : '▼'; |
| 131 |
return sprintf($td, esc_attr($accent), esc_html($arrow . ' ' . number_format_i18n(abs($change)))); |
| 132 |
} |
| 133 |
} |
| 134 |
|