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
2declare(strict_types=1);
3
4namespace SwapAds\Client\Admin;
5
6use SwapAds\Client\Admin\OperatorDashboardHubPage;
7use 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 */
27final 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}