| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 30.00% covered (danger) | 30.00% | 15 / 50 | 20.00% covered (danger) | 20.00% | 1 / 5 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
BacklinksHubPage | 30.00% covered (danger) | 30.00% | 15 / 50 | 20.00% covered (danger) | 20.00% | 1 / 5 | 103.81 | 0.00% covered (danger) | 0.00% | 0 / 1
currentTab | 0.00% covered (danger) | 0.00% | 0 / 2 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
register | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
renderTabs | 100.00% covered (success) | 100.00% | 15 / 15 | 100.00% covered (success) | 100.00% | 1 / 1 | 7 |
addMenu | 0.00% covered (danger) | 0.00% | 0 / 8 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
render | 0.00% covered (danger) | 0.00% | 0 / 24 | 0.00% covered (danger) | 0.00% | 0 / 1 | 30 |

1 | <?php
2 | declare(strict_types=1);
3 |
4 | namespace SwapAds\Client\Admin;
5 |
6 | use SwapAds\Client\Admin\OperatorDashboardHubPage;
7 | use SwapAds\Client\License\LicenseManager;
8 |
9 | /**
10 | * BacklinksHubPage — F200 tabbed UI combining "Your Backlinks" +
11 | * "Place Backlinks" under one menu slug.
12 | *
13 | * Replaces the two separate menu items:
14 | * - "Create Backlink" → BacklinkCreatePage
15 | * - "Backlink Suggestions" → BacklinkApprovalPage
16 | *
17 | * The hub renders a tab navigation based on `?tab=your|place` (default:
18 | * `your`). Each tab delegates its body to the corresponding source
19 | * page (`BacklinkCreatePage::renderFormCard()`,
20 | * `BacklinkApprovalPage::renderSuggestionsCard()`).
21 | *
22 | * The source pages still exist (for back-compat) but their
23 | * `addMenu()` calls are now no-ops.
24 | *
25 | * @since 1.5.2
26 | */
27 | final class BacklinksHubPage
28 | {
29 | public const MENU_SLUG = 'swapads-client-backlinks-hub';
30 |
31 | public const TAB_YOUR = 'your';
32 | public const TAB_PLACE = 'place';
33 | /**
34 | * F120 (2026-08-01): third tab — two-way transparency view of
35 | * approvals of the operator's own backlink offers.
36 | */
37 | public const TAB_APPROVALS = 'approvals';
38 |
39 | /**
40 | * Active tab (sanitized). Defaults to TAB_YOUR.
41 | */
42 | public static function currentTab(): string
43 | {
44 | $t = (string) ($_GET['tab'] ?? self::TAB_YOUR);
45 | return in_array($t, [self::TAB_YOUR, self::TAB_PLACE, self::TAB_APPROVALS], true) ? $t : self::TAB_YOUR;
46 | }
47 |
48 | /**
49 | * Register WP hooks.
50 | *
51 | * @since 1.5.2
52 | */
53 | public static function register(): void
54 | {
55 | add_action('admin_menu', [self::class, 'addMenu']);
56 | }
57 |
58 | /**
59 | * Render the tab nav HTML.
60 | */
61 | public static function renderTabs(string $active): string
62 | {
63 | $your = esc_url(add_query_arg('tab', self::TAB_YOUR));
64 | $place = esc_url(add_query_arg('tab', self::TAB_PLACE));
65 | // F120: third tab — two-way transparency (see TAB_APPROVALS constant).
66 | $approvals = esc_url(add_query_arg('tab', self::TAB_APPROVALS));
67 | $h = '<nav class="swapads-tab-nav" role="tablist" aria-label="Backlinks tabs">';
68 | $h .= '<a href="' . $your . '" role="tab" aria-selected="' . ($active === self::TAB_YOUR ? 'true' : 'false') . '"'
69 | . ' class="swapads-tab' . ($active === self::TAB_YOUR ? ' swapads-tab--active' : '') . '">'
70 | . esc_html__('Your Backlinks', 'swapads-client') . '</a>';
71 | $h .= '<a href="' . $place . '" role="tab" aria-selected="' . ($active === self::TAB_PLACE ? 'true' : 'false') . '"'
72 | . ' class="swapads-tab' . ($active === self::TAB_PLACE ? ' swapads-tab--active' : '') . '">'
73 | . esc_html__('Place Backlinks', 'swapads-client') . '</a>';
74 | $h .= '<a href="' . $approvals . '" role="tab" aria-selected="' . ($active === self::TAB_APPROVALS ? 'true' : 'false') . '"'
75 | . ' class="swapads-tab' . ($active === self::TAB_APPROVALS ? ' swapads-tab--active' : '') . '">'
76 | . esc_html__('Approvals', 'swapads-client') . '</a>';
77 | $h .= '</nav>';
78 | return $h;
79 | }
80 |
81 | /**
82 | * Register the single "Backlinks" submenu.
83 | */
84 | public static function addMenu(): void
85 | {
86 | add_submenu_page(
87 | OperatorDashboardHubPage::MENU_SLUG,
88 | 'Backlinks',
89 | 'Backlinks',
90 | 'manage_options',
91 | self::MENU_SLUG,
92 | [self::class, 'render']
93 | );
94 | }
95 |
96 | /**
97 | * Render the full hub page.
98 | */
99 | public static function render(): void
100 | {
101 | if (!current_user_can('manage_options')) {
102 | wp_die('Insufficient permissions', 'Forbidden', ['response' => 403]);
103 | }
104 |
105 | $tab = self::currentTab();
106 | echo '<div class="wrap swapads-page">';
107 | echo '<h1>' . esc_html__('Backlinks', 'swapads-client') . '</h1>';
108 | echo '<p class="description">'
109 | . esc_html__('Manage your own backlink offers and place backlinks on partner sites.', 'swapads-client')
110 | . '</p>';
111 | echo self::renderTabs($tab);
112 |
113 | if (!LicenseManager::isLicensed()) {
114 | echo Renderer::card(
115 | 'License required',
116 | '<p>You need an active license before managing backlinks.</p>'
117 | . '<p><a href="' . esc_url(admin_url('admin.php?page=' . SettingsPage::MENU_SLUG)) . '" class="button">Activate license</a></p>',
118 | 'swapads-card-warning'
119 | );
120 | echo '</div>'; // .wrap
121 | return;
122 | }
123 |
124 | if ($tab === self::TAB_PLACE) {
125 | BacklinkApprovalPage::renderSuggestionsCard();
126 | } elseif ($tab === self::TAB_APPROVALS) {
127 | // F120 (2026-08-01): delegate to the new ApprovalsHubPage
128 | // (two-way transparency: who approved MY backlinks).
129 | \SwapAds\Client\Admin\ApprovalsHubPage::renderSection();
130 | } else {
131 | BacklinkCreatePage::renderYourBacklinksSection();
132 | }
133 |
134 | echo '</div>'; // .wrap
135 | }
136 | }