用户公开主页全局采用DOM无刷新加载

This commit is contained in:
2026-08-23 14:55:24 +08:00
parent c3f66e9a14
commit bb9a410ad6
14 changed files with 569 additions and 231 deletions
@@ -0,0 +1,51 @@
# 公开用户主页去参数化 + 全链路无刷新加载
## 目标
`/author/demo/` 的 URL 永远干净(无 `?tab=`、`follow_sub`、`follow_page`、`posts_page` 等),Tab 切换、粉丝/关注子切换、各面板分页全部走 REST AJAX 无刷新加载。单路径实现,不写双路/兼容/迁移代码。
现状:主 Tab 已是 DOM 切换,但 `author.js` 会把 `?tab=` 写进 URL;follow 子切换和 comments/follow 分页是带参整页链接;posts/favorites 分页走 `aksrt_render_pagination()`(numbered 模式产出 `?posts_page=N` 链接;load_more/infinite 走 admin-ajax)。
## 服务端
### 1. 上下文构建 — `inc/helpers/aksrt-user-home.php`
- `aksrt_get_user_home_page_context()` 删除全部 `$_GET` 读取(第 104、134-136 行):`active_tab` 恒等于 `default_tab`,三个 page 恒为 1(保留键,供 REST 覆写);新增 `follow_page => 1`、`follow_sub => 'followers'` 键。
- 新增 `aksrt_render_user_home_pagination(int $total, int $current)`:输出 `<nav class="profile-pagination uh-pagination">` + `<button type="button" class="page-numbers" data-uh-page="N">`(当前页带 `current` 类,含 `‹ ›` 前后页),复用现有 `.profile-pagination` 样式(lists.css:331+)。
- 新增 `aksrt_capture_user_home_panel(string $tab, array $ctx): string`:`locate_template('templates/parts/user-home/panel-{$tab}.php')` + ob_start 捕获(仿 `aksrt_capture_user_center_panel`,aksrt-user-center.php:199)。
### 2. 面板模板重构 — `templates/parts/user-home/`
- 删除 `panels-core.php`,拆为按面板 body 模板:`panel-posts.php`、`panel-comments.php`、`panel-favorites.php`、`panel-credits.php`;`panel-follow.php` 改为纯 body(去掉外层 wrapper,第 21-23 行 `$_GET` 读取改为从 `$args` 取 `$follow_page`/`$follow_sub`)。
- `page-user-home.php`:dashboard 根节点加 `data-user-id="<?php echo (int) $user_id; ?>"`;循环 `$enabled_tabs`,对存在 body 模板的 tab 输出 `<div id="panel-{tab}" class="dashboard-panel…">` wrapper + `get_template_part('templates/parts/user-home/panel', $tab, $ctx)`。
- 各 body 分页统一改用 `aksrt_render_user_home_pagination()`:
- posts/favorites 不再用 `aksrt_render_pagination()`(本页退出全站三模式分页体系);
- comments 不再用 `aksrt_render_profile_pagination()`(该函数保留,用户中心仍在用)。
- `panel-follow.php` 子切换 `<a href="?tab=follow&…">`(第 40、47 行)→ `<button type="button" data-uh-sub="followers|following">`;`paginate_links`(第 63-69 行)→ 新分页渲染器。
### 3. REST 端点 — `src/User/Home.php`(仿 `src/User/Center.php:34-110`)
- `init()` 挂 `rest_api_init`;路由 `GET aksrt/v1/user-home/panel/(?P<user_id>\d+)/(?P<tab>[a-z0-9_-]+)`,query 参数 `page`(int ≥1)、`sub`(followers|following),`permission_callback => '__return_true'`(公开数据)。
- `rest_panel()`:校验用户存在 → `aksrt_get_user_home_page_context()` 重建上下文 → 用请求参数覆写 `active_tab`、对应 page、`follow_sub` → tab 不在 `enabled_tabs`(如 credits 非本人)返回 404 → 捕获 body → 返回 `{success:true, data:{tab, html}}`。
## 前端 — `assets/js/author.js`
- 删除 `updateTabUrl()`(第 15-24 行)与 `resolveInitialTab()`(第 70-73 行):不再读写任何 URL 参数,初始态以服务端渲染的 `is-active`/`data-default-tab` 为准。
- 新增 `loadPanelBody(tab, page, sub)`:`fetch(aksrtData.apiUrl + '/user-home/panel/{data-user-id}/{tab}?page=N(&sub=X)', {credentials:'same-origin', headers:{'X-WP-Nonce': aksrtData.restNonce}})` → `panel.innerHTML = data.html` → `AksrtFrontend.rehydrate(panel, 'ajax')`;per-panel 请求去重防竞态(仿 user-center.js `_aksrtPanelRequest`);`is-loading` 态 + 失败 notyf 提示。
- dashboard 根上事件委托(覆盖 AJAX 注入的新按钮):
- `[data-uh-sub]` → 加载 follow 面板 `sub=X, page=1`;
- `[data-uh-page]` → `btn.closest('.dashboard-panel')` 得 tab,`panel.dataset.uhSub`(默认 followers)得子项,加载对应页。
- URL 永不变更(无 pushState/replaceState)。`follow.js` 是 document 级委托,注入的 minicard 关注按钮自动可用,无需改动。
## 附带清理与 CSS
- `templates/parts/submit/editor-main.php:106-108`:「查看草稿」现指向公开主页 + `?tab=post&post_status=draft`(公开主页从未支持该参数,本就错位)。改为 `add_query_arg('post_status', 'draft', aksrt_get_user_center_tab_url('posts'))`(用户中心支持该过滤,posts-list.php:14-16)。
- `assets/css/routes/user-home.css`:`button.page-numbers` 复位 + hover 态(现 hover 选择器只覆盖 `a.page-numbers`);`.uh-follow__subtab` 改 button 后的复位;`.dashboard-panel.is-loading` 半透明过渡。
## 测试与验证
- 更新 `tests/widget-system/post-visual-contract.test.cjs`:移除 `panels-core.php`/`page_var` 断言(第 39、205、222-224、328-330 行),改为断言:各 `panel-*.php` body 模板存在且调用 `aksrt_render_user_home_pagination`;user-home 模板无 `$_GET`/`add_query_arg`;author.js 无 `searchParams`(防回归契约)。
- `php -l` 全部改动 PHP 文件;`node --test tests/widget-system/post-visual-contract.test.cjs`。
## 明确的行为变化(单路径的直接结果)
- 公开主页 posts/favorites 分页不再跟随后台「文章分页模式」(numbered/加载更多/无限滚动)设置,统一为本页 AJAX 数字分页。
- 刷新/分享/回退永远回到默认 Tab 第 1 页(URL 无状态,需求本身决定)。
- 旧外链 `?tab=follow` 不解析不重定向,按默认 Tab 加载(无兼容代码)。
+23
View File
@@ -504,7 +504,12 @@
gap: 4px; gap: 4px;
padding: 4px 12px; padding: 4px 12px;
font-size: 14px; font-size: 14px;
font-family: inherit;
background: none;
border: 0;
color: inherit;
text-decoration: none; text-decoration: none;
cursor: pointer;
transition: color .2s; transition: color .2s;
} }
@@ -567,3 +572,21 @@
.aksrt-profile-details-modal__dialog dt { color: var(--color-muted, #6b7280); } .aksrt-profile-details-modal__dialog dt { color: var(--color-muted, #6b7280); }
.aksrt-profile-details-modal__dialog dd { margin: 0; overflow-wrap: anywhere; } .aksrt-profile-details-modal__dialog dd { margin: 0; overflow-wrap: anywhere; }
.aksrt-profile-details-modal__close { position: absolute; top: 12px; right: 12px; width: 32px; height: 32px; border: 0; border-radius: 50%; background: transparent; font-size: 24px; cursor: pointer; } .aksrt-profile-details-modal__close { position: absolute; top: 12px; right: 12px; width: 32px; height: 32px; border: 0; border-radius: 50%; background: transparent; font-size: 24px; cursor: pointer; }
/* Button-driven panel pagination + fragments (no URL state) */
.aksrt-user-home .profile-pagination button.page-numbers {
font: inherit;
cursor: pointer;
}
.aksrt-user-home .profile-pagination button.page-numbers:hover {
color: var(--color-primary);
border-color: var(--color-primary);
background: var(--color-primary-soft);
}
.aksrt-user-home .dashboard-panel.is-loading {
opacity: .6;
transition: opacity var(--dur-fast, .15s) var(--ease, ease);
pointer-events: none;
}
+77 -21
View File
@@ -1,26 +1,22 @@
/** /**
* AKSRT Public User Home runtime * AKSRT Public User Home runtime
* *
* Handles public user-home dashboard tabs and cover uploads. * Handles public user-home dashboard tabs, panel fragment loading and cover
* Loaded on /user/{nicename}/ (author archives 301-redirect here). * uploads. The URL stays clean (/author/{name}/) — tabs, follow sub-tabs and
* pagination are loaded via the aksrt/v1 user-home panel REST fragment.
* *
* @package AKSRT * @package AKSRT
*/ */
/* global aksrtAuthorPage */ /* global aksrtAuthorPage, aksrtData, AksrtFrontend, notyf */
(function () { (function () {
'use strict'; 'use strict';
function updateTabUrl(root, name, defaultTab) { function notifyLoadError() {
if (!window.history || typeof window.history.replaceState !== 'function') return; if (typeof notyf === 'function') {
var url = new URL(window.location.href); notyf('加载失败,请重试', 'danger');
if (!name || name === defaultTab) {
url.searchParams.delete('tab');
} else {
url.searchParams.set('tab', name);
} }
window.history.replaceState({}, '', url.toString());
} }
function initPublicDashboards() { function initPublicDashboards() {
@@ -32,6 +28,7 @@
if (!tabs.length) return; if (!tabs.length) return;
var defaultTab = root.dataset.defaultTab || tabs[0].getAttribute('data-tab') || 'posts'; var defaultTab = root.dataset.defaultTab || tabs[0].getAttribute('data-tab') || 'posts';
var userId = parseInt(root.dataset.userId || '0', 10);
var panels = {}; var panels = {};
tabs.forEach(function (tab) { tabs.forEach(function (tab) {
@@ -45,7 +42,7 @@
} }
}); });
function switchTo(name, syncUrl) { function switchTo(name) {
if (!panels[name]) { if (!panels[name]) {
name = defaultTab; name = defaultTab;
} }
@@ -61,24 +58,83 @@
panels[key].panel.setAttribute('hidden', ''); panels[key].panel.setAttribute('hidden', '');
} }
}); });
if (syncUrl) {
updateTabUrl(root, name, defaultTab);
}
} }
function resolveInitialTab() { function loadPanelBody(panel, name, page, sub) {
var url = new URL(window.location.href); var cfg = window.aksrtData || {};
return url.searchParams.get('tab') || defaultTab; var restBase = cfg.apiUrl || cfg.restUrl || '';
if (!userId || !restBase) return Promise.reject(new Error('rest-unavailable'));
if (panel._aksrtUhRequest) return panel._aksrtUhRequest;
var followSub = sub || panel.dataset.uhSub || 'followers';
panel.dataset.uhPage = String(page);
panel.dataset.uhSub = followSub;
panel.classList.add('is-loading');
var url = restBase.replace(/\/$/, '') + '/user-home/panel/' + userId + '/' + encodeURIComponent(name)
+ '?page=' + encodeURIComponent(page) + '&sub=' + encodeURIComponent(followSub);
panel._aksrtUhRequest = fetch(url, {
credentials: 'same-origin',
headers: {
'X-WP-Nonce': cfg.restNonce || cfg.nonce || ''
}
}).then(function (response) {
return response.json().then(function (payload) {
if (!response.ok || !payload || !payload.success || !payload.data || typeof payload.data.html !== 'string') {
throw new Error('panel-load-failed');
}
panel.innerHTML = payload.data.html;
if (window.AksrtFrontend && typeof window.AksrtFrontend.rehydrate === 'function') {
window.AksrtFrontend.rehydrate(panel, 'ajax');
}
});
}).finally(function () {
panel._aksrtUhRequest = null;
panel.classList.remove('is-loading');
});
return panel._aksrtUhRequest;
}
function panelNameOf(panel) {
var name = (panel.id || '').replace(/^panel-/, '');
return panels[name] ? name : '';
} }
tabs.forEach(function (tab) { tabs.forEach(function (tab) {
tab.addEventListener('click', function () { tab.addEventListener('click', function () {
switchTo(tab.getAttribute('data-tab'), true); switchTo(tab.getAttribute('data-tab'));
}); });
}); });
switchTo(resolveInitialTab(), false); // Delegate sub-tab / pagination clicks so buttons injected by REST
// fragments keep working without rebinding.
root.addEventListener('click', function (event) {
var target = event.target && event.target.closest ? event.target : null;
if (!target) return;
var subBtn = target.closest('[data-uh-sub]');
if (subBtn) {
event.preventDefault();
var followPanel = subBtn.closest('.dashboard-panel');
if (!followPanel) return;
var sub = subBtn.getAttribute('data-uh-sub') === 'following' ? 'following' : 'followers';
loadPanelBody(followPanel, 'follow', 1, sub).catch(notifyLoadError);
return;
}
var pageBtn = target.closest('[data-uh-page]');
if (!pageBtn) return;
event.preventDefault();
var panel = pageBtn.closest('.dashboard-panel');
if (!panel) return;
var name = panelNameOf(panel);
if (!name) return;
var page = Math.max(1, parseInt(pageBtn.getAttribute('data-uh-page') || '1', 10) || 1);
loadPanelBody(panel, name, page, panel.dataset.uhSub || 'followers').catch(notifyLoadError);
});
switchTo(defaultTab);
}); });
} }
+72 -6
View File
@@ -101,10 +101,7 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
} }
} }
$active_tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : $default_tab;
if (!in_array($active_tab, $enabled_tabs, true)) {
$active_tab = $default_tab; $active_tab = $default_tab;
}
$home_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : home_url('/'); $home_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($user) : home_url('/');
@@ -131,9 +128,11 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
'tab_counts' => $tab_counts, 'tab_counts' => $tab_counts,
'tab_labels' => $tab_labels, 'tab_labels' => $tab_labels,
'home_url' => $home_url, 'home_url' => $home_url,
'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1), 'posts_page' => 1,
'comments_page' => max(1, isset($_GET['comments_page']) ? (int) $_GET['comments_page'] : 1), 'comments_page' => 1,
'favorites_page' => max(1, isset($_GET['favorites_page']) ? (int) $_GET['favorites_page'] : 1), 'favorites_page' => 1,
'follow_sub' => 'followers',
'follow_page' => 1,
'svg_empty_post' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6M9 9h1"/></svg>', 'svg_empty_post' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h6M9 9h1"/></svg>',
'svg_empty_comment' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>', 'svg_empty_comment' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
'svg_empty_favorite' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>', 'svg_empty_favorite' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
@@ -141,3 +140,70 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
]; ];
} }
} }
if (!function_exists('aksrt_render_user_home_pagination')) {
/**
* 渲染公开用户主页面板的无刷新数字分页(按钮驱动,无 URL 参数)。
*
* 复用 .profile-pagination / .page-numbers 既有样式契约。
*
* @param int $total_pages 总页数。
* @param int $current 当前页。
*/
function aksrt_render_user_home_pagination(int $total_pages, int $current): void {
$total_pages = max(1, $total_pages);
$current = min(max(1, $current), $total_pages);
if ($total_pages <= 1) {
return;
}
$items = [];
for ($i = 1; $i <= $total_pages; $i++) {
if ($i === 1 || $i === $total_pages || abs($i - $current) <= 1) {
$items[] = (string) $i;
} elseif ($items && end($items) !== '…') {
$items[] = '…';
}
}
echo '<nav class="profile-pagination uh-pagination" aria-label="' . esc_attr__('分页', 'aksrt') . '">';
if ($current > 1) {
echo '<button type="button" class="page-numbers" data-uh-page="' . esc_attr((string) ($current - 1)) . '"><span aria-hidden="true">&lsaquo;</span><span class="screen-reader-text">' . esc_html__('上一页', 'aksrt') . '</span></button>';
}
foreach ($items as $item) {
if ($item === '…') {
echo '<span class="page-numbers dots">&hellip;</span>';
continue;
}
if ((int) $item === $current) {
echo '<span class="page-numbers current" aria-current="page">' . esc_html($item) . '</span>';
continue;
}
echo '<button type="button" class="page-numbers" data-uh-page="' . esc_attr($item) . '">' . esc_html($item) . '</button>';
}
if ($current < $total_pages) {
echo '<button type="button" class="page-numbers" data-uh-page="' . esc_attr((string) ($current + 1)) . '"><span aria-hidden="true">&rsaquo;</span><span class="screen-reader-text">' . esc_html__('下一页', 'aksrt') . '</span></button>';
}
echo '</nav>';
}
}
if (!function_exists('aksrt_capture_user_home_panel')) {
/**
* 捕获公开用户主页面板 body(templates/parts/user-home/panel-{tab}.php)。
*
* @param string $tab 面板键名。
* @param array<string, mixed> $ctx aksrt_get_user_home_page_context() 上下文。
* @return string 捕获的 HTML,模板不存在时为空串。
*/
function aksrt_capture_user_home_panel(string $tab, array $ctx): string {
$template = locate_template(['templates/parts/user-home/panel-' . sanitize_key($tab) . '.php'], false, false);
if (!$template) {
return '';
}
ob_start();
load_template($template, false, $ctx);
return (string) ob_get_clean();
}
}
+89
View File
@@ -25,6 +25,95 @@ class Home {
public function init(): void { public function init(): void {
$this->register_default_tabs(); $this->register_default_tabs();
add_action('rest_api_init', [$this, 'register_rest_routes']);
}
/**
* Register public user-home panel fragment routes.
*/
public function register_rest_routes(): void {
register_rest_route('aksrt/v1', '/user-home/panel/(?P<user_id>\d+)/(?P<tab>[a-z0-9_-]+)', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'rest_panel'],
'permission_callback' => '__return_true',
'args' => [
'user_id' => [
'required' => true,
'sanitize_callback' => 'absint',
'validate_callback' => static function ($value): bool {
return (int) $value > 0;
},
],
'tab' => [
'required' => true,
'sanitize_callback' => 'sanitize_key',
'validate_callback' => static function ($value): bool {
return preg_match('/^[a-z0-9_-]+$/', (string) $value) === 1;
},
],
'page' => [
'required' => false,
'default' => 1,
'sanitize_callback' => 'absint',
],
'sub' => [
'required' => false,
'default' => 'followers',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => static function ($value): bool {
return in_array((string) $value, ['followers', 'following'], true);
},
],
],
]);
}
/**
* Render a public user-home panel body fragment.
*/
public function rest_panel(\WP_REST_Request $request) {
$user = get_userdata((int) $request->get_param('user_id'));
if (!$user) {
return new \WP_Error('aksrt_user_home_user', __('用户不存在', 'aksrt'), ['status' => 404]);
}
$tab = sanitize_key((string) $request->get_param('tab'));
$page = max(1, (int) $request->get_param('page'));
$sub = (string) $request->get_param('sub') === 'following' ? 'following' : 'followers';
if (!function_exists('aksrt_get_user_home_page_context')) {
require_once AKSRT_DIR . '/inc/helpers/aksrt-user-home.php';
}
$context = aksrt_get_user_home_page_context($user);
if (!in_array($tab, (array) ($context['enabled_tabs'] ?? []), true)) {
return new \WP_Error('aksrt_user_home_panel', __('用户主页面板不存在', 'aksrt'), ['status' => 404]);
}
$context['active_tab'] = $tab;
$context['follow_sub'] = $sub;
$page_keys = [
'posts' => 'posts_page',
'comments' => 'comments_page',
'favorites' => 'favorites_page',
'follow' => 'follow_page',
];
if (isset($page_keys[$tab])) {
$context[$page_keys[$tab]] = $page;
}
$html = aksrt_capture_user_home_panel($tab, $context);
if ($html === '') {
return new \WP_Error('aksrt_user_home_panel_empty', __('用户主页面板暂不可用', 'aksrt'), ['status' => 404]);
}
return rest_ensure_response([
'success' => true,
'data' => [
'tab' => $tab,
'html' => $html,
],
]);
} }
/** /**
+9 -3
View File
@@ -48,7 +48,7 @@ require_once AKSRT_DIR . '/templates/parts/user-shared-helpers.php';
]); ]);
?> ?>
<div class="dashboard dashboard--public" data-default-tab="<?php echo esc_attr($default_tab); ?>"> <div class="dashboard dashboard--public" data-default-tab="<?php echo esc_attr($default_tab); ?>" data-user-id="<?php echo (int) $user_id; ?>">
<div class="dashboard-tabs" role="tablist"> <div class="dashboard-tabs" role="tablist">
<?php foreach ($enabled_tabs as $tab_key) : <?php foreach ($enabled_tabs as $tab_key) :
$tab_class = $tab_key === $active_tab ? 'is-active' : ''; $tab_class = $tab_key === $active_tab ? 'is-active' : '';
@@ -68,9 +68,15 @@ require_once AKSRT_DIR . '/templates/parts/user-shared-helpers.php';
</div> </div>
<?php <?php
get_template_part('templates/parts/user-home/panels', 'core', $ctx); foreach ($enabled_tabs as $panel_tab) :
get_template_part('templates/parts/user-home/panel', 'follow', $ctx); if (!locate_template(['templates/parts/user-home/panel-' . $panel_tab . '.php'], false, false)) {
continue;
}
?> ?>
<div id="panel-<?php echo esc_attr($panel_tab); ?>" class="dashboard-panel<?php echo $panel_tab === $active_tab ? ' is-active' : ''; ?>" role="tabpanel"<?php echo $panel_tab !== $active_tab ? ' hidden' : ''; ?>>
<?php get_template_part('templates/parts/user-home/panel', $panel_tab, $ctx); ?>
</div>
<?php endforeach; ?>
</div> </div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -103,8 +103,8 @@ $is_can_new = !empty($ctx['is_can_new']);
<?php endif; ?> <?php endif; ?>
<?php elseif (!empty($ctx['draft_count']) && $ctx['user_id'] > 0) : ?> <?php elseif (!empty($ctx['draft_count']) && $ctx['user_id'] > 0) : ?>
<?php <?php
$draft_url = function_exists('aksrt_get_user_home_url') $draft_url = function_exists('aksrt_get_user_center_url')
? add_query_arg(['tab' => 'post', 'post_status' => 'draft'], aksrt_get_user_home_url($ctx['user_id'])) ? add_query_arg('post_status', 'draft', aksrt_get_user_center_url('posts'))
: ''; : '';
?> ?>
<?php if ($draft_url !== '') : ?> <?php if ($draft_url !== '') : ?>
@@ -0,0 +1,39 @@
<?php
/**
* Public user home — comments tab panel body.
*
* Rendered inside the #panel-comments wrapper on the public user home and via
* the aksrt/v1 user-home panel REST fragment. Pagination is button-driven
* (aksrt_render_user_home_pagination) and never touches the URL.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$args = is_array($args ?? null) ? $args : [];
extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
$user_comments = get_comments([
'user_id' => $user_id,
'status' => 'approve',
'number' => $per_page,
'offset' => ($comments_page - 1) * $per_page,
'type' => 'comment',
'update_comment_meta_cache' => false,
]);
if ($user_comments) :
?>
<div class="profile-comment-list">
<?php foreach ($user_comments as $c) : ?>
<?php
get_template_part('templates/parts/content', 'profile-comment-item', ['comment' => $c]);
?>
<?php endforeach; ?>
</div>
<?php
aksrt_render_user_home_pagination((int) ceil($comment_count / max(1, $per_page)), (int) $comments_page);
else :
aksrt_render_profile_empty($svg_empty_comment, __('TA 还没有发表评论', 'aksrt'));
endif;
@@ -0,0 +1,25 @@
<?php
/**
* Public user home — credits tab panel body (own profile only).
*
* Rendered inside the #panel-credits wrapper on the public user home. Static
* balance card; no pagination, no REST swapping needed.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$args = is_array($args ?? null) ? $args : [];
extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
if (empty($credits_enabled) || empty($is_own_profile)) {
return;
}
?>
<div class="user-home-balance-card">
<p class="user-home-balance-card__label"><?php echo esc_html(sprintf(__('当前%s余额', 'aksrt'), $currency_name)); ?></p>
<div class="user-home-balance-card__value"><?php echo esc_html(aksrt_format_credits($credit_balance)); ?></div>
<p class="user-home-balance-card__hint"><?php esc_html_e('完整账单和赚取/消费记录请在用户中心查看。', 'aksrt'); ?></p>
<a href="<?php echo esc_url(aksrt_get_user_center_url('credits')); ?>" class="btn-outline btn-sm"><?php esc_html_e('进入用户中心', 'aksrt'); ?></a>
</div>
@@ -0,0 +1,43 @@
<?php
/**
* Public user home — favorites tab panel body.
*
* Rendered inside the #panel-favorites wrapper on the public user home and via
* the aksrt/v1 user-home panel REST fragment. Pagination is button-driven
* (aksrt_render_user_home_pagination) and never touches the URL.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$args = is_array($args ?? null) ? $args : [];
extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
if (!empty($favorite_post_ids)) :
$user_home_render_config = aksrt_resolve_post_list_render_config([
'context' => 'author',
'overrides' => [
'type' => 'list',
'no_author' => true,
'no_margin' => true,
],
]);
$favorites_query = new WP_Query([
'post_type' => 'post',
'post_status' => 'publish',
'post__in' => $favorite_post_ids,
'orderby' => 'post__in',
'posts_per_page' => $per_page,
'paged' => $favorites_page,
'ignore_sticky_posts' => true,
'no_found_rows' => false,
'update_post_meta_cache' => false,
]);
aksrt_render_post_list(array_merge($user_home_render_config, ['wrapper_class' => 'aksrt-user-home__post-list']), $favorites_query);
aksrt_render_user_home_pagination((int) $favorites_query->max_num_pages, (int) $favorites_page);
wp_reset_postdata();
else :
aksrt_render_profile_empty($svg_empty_favorite, __('TA 还没有收藏文章', 'aksrt'));
endif;
+26 -43
View File
@@ -1,11 +1,15 @@
<?php <?php
/** /**
* Public user home — follow tab panel (social following/followers). * Public user home — follow tab panel body (social following/followers).
* *
* Legacy parity: 粉丝/关注用户列表复刻 zib_get_follow_user_list + zib_author_card * Legacy parity: 粉丝/关注用户列表复刻 zib_get_follow_user_list + zib_author_card
* (inc/functions/zib-author.php:395-454, 685-716):子切换「粉丝 | 关注」居中文字链接, * (inc/functions/zib-author.php:395-454, 685-716):子切换「粉丝 | 关注」居中文字链接,
* 列表项走 aksrt_author_minicard(.author-minicard 三列 inline-block 卡片流)。 * 列表项走 aksrt_author_minicard(.author-minicard 三列 inline-block 卡片流)。
* *
* Rendered inside the #panel-follow wrapper on the public user home and via
* the aksrt/v1 user-home panel REST fragment. Sub-tabs and pagination are
* button-driven ([data-uh-sub] / [data-uh-page]) and never touch the URL.
*
* @package AKSRT * @package AKSRT
*/ */
@@ -14,69 +18,48 @@ defined('ABSPATH') || exit;
$args = is_array($args ?? null) ? $args : []; $args = is_array($args ?? null) ? $args : [];
extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
?> $follow_page = max(1, (int) ($follow_page ?? 1));
<?php if (in_array('follow', $enabled_tabs, true)) : ?> $follow_sub = in_array($follow_sub ?? 'followers', ['following', 'followers'], true) ? $follow_sub : 'followers';
<div id="panel-follow" class="dashboard-panel<?php echo $active_tab === 'follow' ? ' is-active' : ''; ?>" role="tabpanel"<?php echo $active_tab !== 'follow' ? ' hidden' : ''; ?>>
<?php
$follow_page = max(1, isset($_GET['follow_page']) ? (int) $_GET['follow_page'] : 1);
$follow_sub = isset($_GET['follow_sub']) ? sanitize_key(wp_unslash($_GET['follow_sub'])) : 'followers';
$follow_sub = in_array($follow_sub, ['following', 'followers'], true) ? $follow_sub : 'followers';
$follow_instance = class_exists('\AKSRT\User\Follow') ? \AKSRT\User\Follow::get_instance() : null; $follow_instance = class_exists('\AKSRT\User\Follow') ? \AKSRT\User\Follow::get_instance() : null;
$following_count = $follow_instance ? (int) $follow_instance->get_following_count((int) $user_id) : 0; $following_count = $follow_instance ? (int) $follow_instance->get_following_count((int) $user_id) : 0;
$follower_count = $follow_instance ? (int) $follow_instance->get_follower_count((int) $user_id) : 0; $follower_count = $follow_instance ? (int) $follow_instance->get_follower_count((int) $user_id) : 0;
$list = ['items' => [], 'total' => 0, 'total_pages' => 1]; $list = ['items' => [], 'total' => 0, 'total_pages' => 1];
if ($follow_instance) { if ($follow_instance) {
$list = $follow_sub === 'followers' $list = $follow_sub === 'followers'
? $follow_instance->get_followers((int) $user_id, $follow_page, (int) $per_page) ? $follow_instance->get_followers((int) $user_id, $follow_page, (int) $per_page)
: $follow_instance->get_following((int) $user_id, $follow_page, (int) $per_page); : $follow_instance->get_following((int) $user_id, $follow_page, (int) $per_page);
} }
$items = is_array($list['items'] ?? null) ? $list['items'] : []; $items = is_array($list['items'] ?? null) ? $list['items'] : [];
$total_pages = max(1, (int) ($list['total_pages'] ?? 1)); $total_pages = max(1, (int) ($list['total_pages'] ?? 1));
?> ?>
<ul class="uh-follow__subtabs list-inline splitters"> <ul class="uh-follow__subtabs list-inline splitters">
<li> <li>
<a class="uh-follow__subtab<?php echo $follow_sub === 'followers' ? ' is-active focus-color' : ' muted-color'; ?>" <button type="button" class="uh-follow__subtab<?php echo $follow_sub === 'followers' ? ' is-active focus-color' : ' muted-color'; ?>" data-uh-sub="followers">
href="<?php echo esc_url(add_query_arg(['tab' => 'follow', 'follow_sub' => 'followers', 'follow_page' => 1], $home_url)); ?>">
<?php esc_html_e('粉丝', 'aksrt'); ?> <?php esc_html_e('粉丝', 'aksrt'); ?>
<span class="uh-follow__subtab-count"><?php echo esc_html((string) $follower_count); ?></span> <span class="uh-follow__subtab-count"><?php echo esc_html((string) $follower_count); ?></span>
</a> </button>
</li> </li>
<li> <li>
<a class="uh-follow__subtab<?php echo $follow_sub === 'following' ? ' is-active focus-color' : ' muted-color'; ?>" <button type="button" class="uh-follow__subtab<?php echo $follow_sub === 'following' ? ' is-active focus-color' : ' muted-color'; ?>" data-uh-sub="following">
href="<?php echo esc_url(add_query_arg(['tab' => 'follow', 'follow_sub' => 'following', 'follow_page' => 1], $home_url)); ?>">
<?php esc_html_e('关注', 'aksrt'); ?> <?php esc_html_e('关注', 'aksrt'); ?>
<span class="uh-follow__subtab-count"><?php echo esc_html((string) $following_count); ?></span> <span class="uh-follow__subtab-count"><?php echo esc_html((string) $following_count); ?></span>
</a> </button>
</li> </li>
</ul> </ul>
<?php if (!empty($items)) : ?> <?php if (!empty($items)) : ?>
<div class="uh-follow__list"> <div class="uh-follow__list">
<?php foreach ($items as $row) : <?php foreach ($items as $row) :
echo aksrt_author_minicard((int) ($row['user_id'] ?? 0)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo aksrt_author_minicard((int) ($row['user_id'] ?? 0)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
endforeach; ?> endforeach; ?>
</div> </div>
<?php if ($total_pages > 1) : ?> <?php aksrt_render_user_home_pagination($total_pages, $follow_page); ?>
<div class="uh-follow__pagination"> <?php else : ?>
<?php
echo paginate_links([
'base' => add_query_arg(['tab' => 'follow', 'follow_sub' => $follow_sub, 'follow_page' => '%#%'], $home_url),
'format' => '',
'current' => $follow_page,
'total' => $total_pages,
'add_args' => ['tab' => 'follow', 'follow_sub' => $follow_sub],
]);
?>
</div>
<?php endif; ?>
<?php else : ?>
<?php <?php
$empty_msg = $follow_sub === 'followers' $empty_msg = $follow_sub === 'followers'
? __('暂无粉丝', 'aksrt') ? __('暂无粉丝', 'aksrt')
: __('暂未关注任何人', 'aksrt'); : __('暂未关注任何人', 'aksrt');
aksrt_render_profile_empty($svg_empty_follow ?? $svg_empty_post ?? '', $empty_msg); aksrt_render_profile_empty($svg_empty_follow ?? $svg_empty_post ?? '', $empty_msg);
?> ?>
<?php endif; ?> <?php endif; ?>
</div>
<?php endif; ?>
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Public user home — posts tab panel body.
*
* Rendered inside the #panel-posts wrapper on the public user home and via
* the aksrt/v1 user-home panel REST fragment. Pagination is button-driven
* (aksrt_render_user_home_pagination) and never touches the URL.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$args = is_array($args ?? null) ? $args : [];
extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
$user_home_render_config = aksrt_resolve_post_list_render_config([
'context' => 'author',
'overrides' => [
'type' => 'list',
'no_author' => true,
'no_margin' => true,
],
]);
$author_posts = new WP_Query([
'author' => $user_id,
'posts_per_page' => $per_page,
'post_status' => 'publish',
'paged' => $posts_page,
'no_found_rows' => false,
]);
aksrt_render_post_list(array_merge($user_home_render_config, ['wrapper_class' => 'aksrt-user-home__post-list']), $author_posts);
aksrt_render_user_home_pagination((int) $author_posts->max_num_pages, (int) $posts_page);
wp_reset_postdata();
-118
View File
@@ -1,118 +0,0 @@
<?php
/**
* Public user home — core content panels (posts/comments/favorites/credits/placeholders).
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$args = is_array($args ?? null) ? $args : [];
extract($args, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
?>
<?php if (in_array('posts', $enabled_tabs, true) || in_array('favorites', $enabled_tabs, true)) : ?>
<?php
$user_home_render_config = aksrt_resolve_post_list_render_config([
'context' => 'author',
'overrides' => [
'type' => 'list',
'no_author' => true,
'no_margin' => true,
],
]);
?>
<?php endif; ?>
<?php if (in_array('posts', $enabled_tabs, true)) : ?>
<div id="panel-posts" class="dashboard-panel<?php echo $active_tab === 'posts' ? ' is-active' : ''; ?>" role="tabpanel"<?php echo $active_tab !== 'posts' ? ' hidden' : ''; ?>>
<?php
$author_posts = new WP_Query([
'author' => $user_id,
'posts_per_page' => $per_page,
'post_status' => 'publish',
'paged' => $posts_page,
'no_found_rows' => false,
]);
aksrt_render_post_list(array_merge($user_home_render_config, ['wrapper_class' => 'aksrt-user-home__post-list']), $author_posts);
aksrt_render_pagination($author_posts, [
'query_context' => 'author',
'render_config' => $user_home_render_config,
'page_var' => 'posts_page',
'current_page' => $posts_page,
]);
wp_reset_postdata();
?>
</div>
<?php endif; ?>
<?php if (in_array('comments', $enabled_tabs, true)) : ?>
<div id="panel-comments" class="dashboard-panel<?php echo $active_tab === 'comments' ? ' is-active' : ''; ?>" role="tabpanel"<?php echo $active_tab !== 'comments' ? ' hidden' : ''; ?>>
<?php
$user_comments = get_comments([
'user_id' => $user_id,
'status' => 'approve',
'number' => $per_page,
'offset' => ($comments_page - 1) * $per_page,
'type' => 'comment',
'update_comment_meta_cache' => false,
]);
if ($user_comments) :
?>
<div class="profile-comment-list">
<?php foreach ($user_comments as $c) : ?>
<?php
get_template_part('templates/parts/content', 'profile-comment-item', ['comment' => $c]);
?>
<?php endforeach; ?>
</div>
<?php
$cm_total_pages = (int) ceil($comment_count / $per_page);
aksrt_render_profile_pagination($cm_total_pages, $comments_page, 'comments_page');
else :
aksrt_render_profile_empty($svg_empty_comment, __('TA 还没有发表评论', 'aksrt'));
endif;
?>
</div>
<?php endif; ?>
<?php if (in_array('favorites', $enabled_tabs, true)) : ?>
<div id="panel-favorites" class="dashboard-panel<?php echo $active_tab === 'favorites' ? ' is-active' : ''; ?>" role="tabpanel"<?php echo $active_tab !== 'favorites' ? ' hidden' : ''; ?>>
<?php if (!empty($favorite_post_ids)) : ?>
<?php
$favorites_query = new WP_Query([
'post_type' => 'post',
'post_status' => 'publish',
'post__in' => $favorite_post_ids,
'orderby' => 'post__in',
'posts_per_page' => $per_page,
'paged' => $favorites_page,
'ignore_sticky_posts' => true,
'no_found_rows' => false,
'update_post_meta_cache' => false,
]);
aksrt_render_post_list(array_merge($user_home_render_config, ['wrapper_class' => 'aksrt-user-home__post-list']), $favorites_query);
aksrt_render_pagination($favorites_query, [
'query_context' => 'author',
'render_config' => $user_home_render_config,
'page_var' => 'favorites_page',
'current_page' => $favorites_page,
]);
wp_reset_postdata();
?>
<?php else : ?>
<?php aksrt_render_profile_empty($svg_empty_favorite, __('TA 还没有收藏文章', 'aksrt')); ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (in_array('credits', $enabled_tabs, true) && $credits_enabled && $is_own_profile) : ?>
<div id="panel-credits" class="dashboard-panel<?php echo $active_tab === 'credits' ? ' is-active' : ''; ?>" role="tabpanel"<?php echo $active_tab !== 'credits' ? ' hidden' : ''; ?>>
<div class="user-home-balance-card">
<p class="user-home-balance-card__label"><?php echo esc_html(sprintf(__('当前%s余额', 'aksrt'), $currency_name)); ?></p>
<div class="user-home-balance-card__value"><?php echo esc_html(aksrt_format_credits($credit_balance)); ?></div>
<p class="user-home-balance-card__hint"><?php esc_html_e('完整账单和赚取/消费记录请在用户中心查看。', 'aksrt'); ?></p>
<a href="<?php echo esc_url(aksrt_get_user_center_url('credits')); ?>" class="btn-outline btn-sm"><?php esc_html_e('进入用户中心', 'aksrt'); ?></a>
</div>
</div>
<?php endif; ?>
@@ -36,7 +36,8 @@ test('the article stream exposes only the new typed renderer contract', () => {
'src/Core/Shortcodes.php', 'src/Core/Shortcodes.php',
'src/WidgetSystem/PostRenderer.php', 'src/WidgetSystem/PostRenderer.php',
'inc/helpers/homepage/content.php', 'inc/helpers/homepage/content.php',
'templates/parts/user-home/panels-core.php', 'templates/parts/user-home/panel-posts.php',
'templates/parts/user-home/panel-favorites.php',
]); ]);
assert.match(markup, /function aksrt_render_post_list\(array \$args = \[\], \?WP_Query \$query = null, bool \$echo = true\): string/); assert.match(markup, /function aksrt_render_post_list\(array \$args = \[\], \?WP_Query \$query = null, bool \$echo = true\): string/);
@@ -202,8 +203,7 @@ test('related posts, author page and widgets use their dedicated renderers', ()
const related = read('inc/helpers/aksrt-single-render.php'); const related = read('inc/helpers/aksrt-single-render.php');
const relatedCard = read('inc/helpers/aksrt-single-components.php'); const relatedCard = read('inc/helpers/aksrt-single-components.php');
const resourceRelated = read('templates/parts/resource-related.php'); const resourceRelated = read('templates/parts/resource-related.php');
const author = read('templates/parts/user-home/panels-core.php'); const author = read('templates/parts/user-home/panel-posts.php');
const authorPosts = author.slice(author.indexOf("in_array('posts'"), author.indexOf("in_array('comments'"));
const widgets = read('src/WidgetSystem/PostRenderer.php'); const widgets = read('src/WidgetSystem/PostRenderer.php');
assert.match(related, /\['img', 'list', 'text'\]/); assert.match(related, /\['img', 'list', 'text'\]/);
@@ -215,14 +215,13 @@ test('related posts, author page and widgets use their dedicated renderers', ()
assert.match(resourceRelated, /swiper-container swiper-scroll/); assert.match(resourceRelated, /swiper-container swiper-scroll/);
assert.match(resourceRelated, /aksrt_render_related_graphic_card/); assert.match(resourceRelated, /aksrt_render_related_graphic_card/);
assert.doesNotMatch(resourceRelated, /aksrt_render_post_list/); assert.doesNotMatch(resourceRelated, /aksrt_render_post_list/);
assert.match(authorPosts, /aksrt_render_post_list/); assert.match(author, /aksrt_render_post_list/);
assert.match(authorPosts, /'type'\s*=>\s*'list'/); assert.match(author, /'type'\s*=>\s*'list'/);
assert.match(authorPosts, /'no_author'\s*=>\s*true/); assert.match(author, /'no_author'\s*=>\s*true/);
assert.match(authorPosts, /'no_margin'\s*=>\s*true/); assert.match(author, /'no_margin'\s*=>\s*true/);
assert.match(authorPosts, /'wrapper_class'\s*=>\s*'aksrt-user-home__post-list'/); assert.match(author, /'wrapper_class'\s*=>\s*'aksrt-user-home__post-list'/);
assert.match(authorPosts, /aksrt_render_pagination/); assert.match(author, /aksrt_render_user_home_pagination\(\(int\) \$author_posts->max_num_pages, \(int\) \$posts_page\)/);
assert.match(authorPosts, /'page_var'\s*=>\s*'posts_page'/); assert.doesNotMatch(author, /aksrt_render_pagination|aksrt_render_profile_pagination|\$_GET|add_query_arg/);
assert.doesNotMatch(authorPosts, /aksrt_render_profile_pagination/);
assert.match(widgets, /aksrt_render_mini_list/); assert.match(widgets, /aksrt_render_mini_list/);
assert.match(widgets, /swiper-container swiper-scroll/); assert.match(widgets, /swiper-container swiper-scroll/);
}); });
@@ -325,10 +324,10 @@ test('profile routes load versioned leaf styles and keep dialogs hidden initiall
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\);/); assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list\s*\{[^}]*display:\s*grid;[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\);/);
assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list > \.posts-item\s*\{[^}]*width:\s*auto;[^}]*margin:\s*0 !important;/); assert.match(userHomeCss, /@media \(min-width: 992px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list > \.posts-item\s*\{[^}]*width:\s*auto;[^}]*margin:\s*0 !important;/);
assert.match(userHomeCss, /@media \(max-width: 767px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list \.posts-item\s*\{[^}]*padding:\s*10px 0;/); assert.match(userHomeCss, /@media \(max-width: 767px\)\s*\{[\s\S]*?\.aksrt-user-home \.aksrt-user-home__post-list \.posts-item\s*\{[^}]*padding:\s*10px 0;/);
const panelsCore = read('templates/parts/user-home/panels-core.php'); const panelFavorites = read('templates/parts/user-home/panel-favorites.php');
assert.match(panelsCore, /'page_var'\s*=>\s*'favorites_page'/); assert.match(panelFavorites, /aksrt_render_user_home_pagination\(\(int\) \$favorites_query->max_num_pages, \(int\) \$favorites_page\)/);
assert.match(panelsCore, /array_merge\(\$user_home_render_config, \['wrapper_class' => 'aksrt-user-home__post-list'\]\), \$favorites_query\)/); assert.match(panelFavorites, /array_merge\(\$user_home_render_config, \['wrapper_class' => 'aksrt-user-home__post-list'\]\), \$favorites_query\)/);
assert.doesNotMatch(panelsCore, /profile-list-item/); assert.doesNotMatch(panelFavorites, /profile-list-item|\$_GET|add_query_arg/);
assert.match(profileLayoutCss, /\.aksrt-cover-menu__panel\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/); assert.match(profileLayoutCss, /\.aksrt-cover-menu__panel\[hidden\]\s*\{\s*display:\s*none\s*!important;\s*\}/);
assert.match(profileLayoutCss, /#aksrt-bg-file-input\[hidden\],[\s\S]*?#aksrt-avatar-file-input\[hidden\][\s\S]*?display:\s*none\s*!important;/); assert.match(profileLayoutCss, /#aksrt-bg-file-input\[hidden\],[\s\S]*?#aksrt-avatar-file-input\[hidden\][\s\S]*?display:\s*none\s*!important;/);
assert.match(settingsSecurity, /data-modal="email-modal"[\s\S]*?<\/button>\s*<\/div>\s*<div class="security-action-item">[\s\S]*?data-modal="phone-modal"/); assert.match(settingsSecurity, /data-modal="email-modal"[\s\S]*?<\/button>\s*<\/div>\s*<div class="security-action-item">[\s\S]*?data-modal="phone-modal"/);
@@ -337,3 +336,44 @@ test('profile routes load versioned leaf styles and keep dialogs hidden initiall
assert.doesNotMatch(accountModals, /id="phone-modal"[\s\S]*?aksrt-modal__(?:dialog|body|actions)/); assert.doesNotMatch(accountModals, /id="phone-modal"[\s\S]*?aksrt-modal__(?:dialog|body|actions)/);
assert.match(profileModalsCss, /\.modal-field input\[type="tel"\]/); assert.match(profileModalsCss, /\.modal-field input\[type="tel"\]/);
}); });
test('public user home stays param-free and swaps panels via REST fragments', () => {
const ctx = read('inc/helpers/aksrt-user-home.php');
const shell = read('templates/pages/page-user-home.php');
const follow = read('templates/parts/user-home/panel-follow.php');
const comments = read('templates/parts/user-home/panel-comments.php');
const favorites = read('templates/parts/user-home/panel-favorites.php');
const rest = read('src/User/Home.php');
const authorJs = read('assets/js/author.js');
// Context builder owns no request state; pages default to 1.
assert.doesNotMatch(ctx, /\$_GET/);
assert.match(ctx, /function aksrt_render_user_home_pagination\(int \$total_pages, int \$current\): void/);
assert.match(ctx, /data-uh-page/);
assert.match(ctx, /function aksrt_capture_user_home_panel\(string \$tab, array \$ctx\): string/);
// Shell mounts panel wrappers from the tab registry with a user id hook.
assert.match(shell, /data-user-id="<\?php echo \(int\) \$user_id; \?>">/);
assert.match(shell, /get_template_part\('templates\/parts\/user-home\/panel', \$panel_tab, \$ctx\)/);
// Follow panel: button-driven sub-tabs, pagination without URL building.
assert.match(follow, /data-uh-sub="followers"/);
assert.match(follow, /data-uh-sub="following"/);
assert.match(follow, /aksrt_render_user_home_pagination\(\$total_pages, \$follow_page\)/);
assert.doesNotMatch(follow, /add_query_arg|\$_GET|paginate_links/);
assert.match(comments, /aksrt_render_user_home_pagination\(/);
assert.match(favorites, /aksrt_render_user_home_pagination\(/);
assert.doesNotMatch(comments + favorites, /add_query_arg|\$_GET/);
// REST fragment endpoint serves public panel bodies.
assert.ok(rest.includes("'/user-home/panel/(?P<user_id>\\d+)/(?P<tab>[a-z0-9_-]+)'"));
assert.match(rest, /'permission_callback'\s*=>\s*'__return_true'/);
assert.match(rest, /aksrt_capture_user_home_panel\(\$tab, \$context\)/);
// Runtime never reads or writes URL params; fragments replace panel bodies.
assert.doesNotMatch(authorJs, /searchParams|pushState|replaceState/);
assert.match(authorJs, /\/user-home\/panel\/' \+ userId \+ '/);
assert.match(authorJs, /data-uh-sub/);
assert.match(authorJs, /AksrtFrontend\.rehydrate\(panel, 'ajax'\)/);
});